kopytko-formatter 0.1.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 +21 -0
- package/README.md +141 -0
- package/dist/bin/kopytko-format.d.ts +3 -0
- package/dist/bin/kopytko-format.d.ts.map +1 -0
- package/dist/bin/kopytko-format.js +356 -0
- package/dist/bin/kopytko-format.js.map +1 -0
- package/dist/src/builtins.d.ts +20 -0
- package/dist/src/builtins.d.ts.map +1 -0
- package/dist/src/builtins.js +139 -0
- package/dist/src/builtins.js.map +1 -0
- package/dist/src/casing.d.ts +29 -0
- package/dist/src/casing.d.ts.map +1 -0
- package/dist/src/casing.js +50 -0
- package/dist/src/casing.js.map +1 -0
- package/dist/src/config.d.ts +129 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +161 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/formatter.d.ts +23 -0
- package/dist/src/formatter.d.ts.map +1 -0
- package/dist/src/formatter.js +1064 -0
- package/dist/src/formatter.js.map +1 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +31 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/types.d.ts +6 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +3 -0
- package/dist/src/types.js.map +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,1064 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatText = formatText;
|
|
4
|
+
exports.checkFormatting = checkFormatting;
|
|
5
|
+
const builtins_1 = require("./builtins");
|
|
6
|
+
const casing_1 = require("./casing");
|
|
7
|
+
/** Canonical (catalog-cased) lookup tables, built once at module load. */
|
|
8
|
+
const _builtinMap = new Map(builtins_1.BRIGHTSCRIPT_BUILTINS.map((b) => [b.name.toLowerCase(), b.name]));
|
|
9
|
+
const _keywordSet = new Set(builtins_1.BRIGHTSCRIPT_KEYWORDS.map((k) => k.toLowerCase()));
|
|
10
|
+
/** Mapping from compact end-keyword to spaced form. */
|
|
11
|
+
const COMPACT_TO_SPACED = {
|
|
12
|
+
'endif': 'end if',
|
|
13
|
+
'endfunction': 'end function',
|
|
14
|
+
'endsub': 'end sub',
|
|
15
|
+
'endwhile': 'end while',
|
|
16
|
+
'endfor': 'end for',
|
|
17
|
+
'endtry': 'end try',
|
|
18
|
+
};
|
|
19
|
+
/** Reverse mapping: spaced form → compact. */
|
|
20
|
+
const SPACED_TO_COMPACT = Object.fromEntries(Object.entries(COMPACT_TO_SPACED).map(([k, v]) => [v, k]));
|
|
21
|
+
/**
|
|
22
|
+
* Formats BrightScript source code using an 11-pass engine.
|
|
23
|
+
*
|
|
24
|
+
* This is the pure, framework-agnostic formatting function.
|
|
25
|
+
* It takes a source string and returns the formatted result.
|
|
26
|
+
*
|
|
27
|
+
* @param source - The raw BrightScript source text.
|
|
28
|
+
* @param config - Formatting rules configuration.
|
|
29
|
+
* @param casing - Casing rules configuration (optional).
|
|
30
|
+
* @param userFunctions - Known user-defined functions for casing normalization (optional).
|
|
31
|
+
* @returns The formatted source text.
|
|
32
|
+
*/
|
|
33
|
+
function formatText(source, config, casing = casing_1.DEFAULT_CASING_CONFIG, userFunctions = []) {
|
|
34
|
+
let lines = source.split(/\r?\n/);
|
|
35
|
+
const detectedEnding = detectLineEnding(source);
|
|
36
|
+
const lineEndStr = resolveLineEnding(config.lineEnding, detectedEnding);
|
|
37
|
+
const userFuncMap = new Map();
|
|
38
|
+
for (const fn of userFunctions) {
|
|
39
|
+
if (!userFuncMap.has(fn.nameLower))
|
|
40
|
+
userFuncMap.set(fn.nameLower, fn.name);
|
|
41
|
+
}
|
|
42
|
+
// Pass 1 — Import sorting
|
|
43
|
+
lines = passImportSorting(lines, config);
|
|
44
|
+
// Pass 2 — Comment normalization
|
|
45
|
+
lines = passCommentNormalization(lines, config);
|
|
46
|
+
// Pass 3 — End keyword style + function vs sub
|
|
47
|
+
lines = passEndKeywordStyle(lines, config);
|
|
48
|
+
lines = passFunctionVsSub(lines, config);
|
|
49
|
+
// Pass 4 — Then style + parenthesis if case
|
|
50
|
+
lines = passThenStyle(lines, config);
|
|
51
|
+
lines = passParenthesisIfCase(lines, config);
|
|
52
|
+
// Pass 5 — Print statement handling
|
|
53
|
+
lines = passPrintStatement(lines, config);
|
|
54
|
+
// Pass 6 — Spacing rules
|
|
55
|
+
lines = passSpacing(lines, config);
|
|
56
|
+
// Pass 7 — Casing
|
|
57
|
+
lines = passCasing(lines, casing, userFuncMap);
|
|
58
|
+
// Pass 7b — Split array open bracket
|
|
59
|
+
lines = passSplitArrayOpenBracket(lines, config);
|
|
60
|
+
// Pass 8 — Indentation
|
|
61
|
+
lines = passIndentation(lines, config);
|
|
62
|
+
// Pass 8b — Trailing commas
|
|
63
|
+
lines = passTrailingCommas(lines, config);
|
|
64
|
+
// Pass 9 — Blank line rules
|
|
65
|
+
lines = passBlankLines(lines, config);
|
|
66
|
+
// Pass 10 — Trailing whitespace
|
|
67
|
+
lines = passTrimTrailing(lines, config);
|
|
68
|
+
// Pass 11 — Comment width
|
|
69
|
+
lines = passCommentWidth(lines, config);
|
|
70
|
+
// Assemble result
|
|
71
|
+
let newText = lines.join(lineEndStr);
|
|
72
|
+
if (config.insertFinalNewline && newText.length > 0 && !newText.endsWith(lineEndStr)) {
|
|
73
|
+
newText += lineEndStr;
|
|
74
|
+
}
|
|
75
|
+
return newText;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Checks whether source text is already formatted.
|
|
79
|
+
*
|
|
80
|
+
* @returns `true` if the text matches formatted output (no changes needed).
|
|
81
|
+
*/
|
|
82
|
+
function checkFormatting(source, config, casing, userFunctions) {
|
|
83
|
+
return formatText(source, config, casing, userFunctions) === source;
|
|
84
|
+
}
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Line ending helpers
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
function detectLineEnding(text) {
|
|
89
|
+
const crlfCount = (text.match(/\r\n/g) || []).length;
|
|
90
|
+
const lfCount = (text.match(/(?<!\r)\n/g) || []).length;
|
|
91
|
+
return crlfCount > lfCount ? '\r\n' : '\n';
|
|
92
|
+
}
|
|
93
|
+
function resolveLineEnding(setting, detected) {
|
|
94
|
+
if (setting === 'lf')
|
|
95
|
+
return '\n';
|
|
96
|
+
if (setting === 'crlf')
|
|
97
|
+
return '\r\n';
|
|
98
|
+
return detected;
|
|
99
|
+
}
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Pass 1 — Import sorting
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
function passImportSorting(lines, config) {
|
|
104
|
+
if (!config.sortImports && !config.emptyLineAfterImports)
|
|
105
|
+
return lines;
|
|
106
|
+
const importRegex = /^\s*'\s*@import\s+/;
|
|
107
|
+
const mockRegex = /^\s*'\s*@mock\s+/;
|
|
108
|
+
const annotationRegex = /^\s*'\s*@(?:import|mock)\s+/;
|
|
109
|
+
let lastAnnotationIdx = -1;
|
|
110
|
+
for (let i = 0; i < lines.length; i++) {
|
|
111
|
+
if (annotationRegex.test(lines[i])) {
|
|
112
|
+
lastAnnotationIdx = i;
|
|
113
|
+
}
|
|
114
|
+
else if (lines[i].trim() !== '') {
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (lastAnnotationIdx < 0)
|
|
119
|
+
return lines;
|
|
120
|
+
const blockEnd = lastAnnotationIdx + 1;
|
|
121
|
+
if (!config.sortImports) {
|
|
122
|
+
if (!config.emptyLineAfterImports)
|
|
123
|
+
return lines;
|
|
124
|
+
const rest = lines.slice(blockEnd);
|
|
125
|
+
if (rest.length > 0 && rest[0].trim() !== '') {
|
|
126
|
+
return [...lines.slice(0, blockEnd), '', ...rest];
|
|
127
|
+
}
|
|
128
|
+
return lines;
|
|
129
|
+
}
|
|
130
|
+
const importLines = [];
|
|
131
|
+
const mockLines = [];
|
|
132
|
+
for (let i = 0; i < blockEnd; i++) {
|
|
133
|
+
const trimmed = lines[i].trim();
|
|
134
|
+
if (mockRegex.test(trimmed))
|
|
135
|
+
mockLines.push(trimmed);
|
|
136
|
+
else if (importRegex.test(trimmed))
|
|
137
|
+
importLines.push(trimmed);
|
|
138
|
+
}
|
|
139
|
+
const fromImportRegex = /^\s*'\s*@import\s+(.*?)\s+from\s+(\S+)\s*$/;
|
|
140
|
+
const moduleImports = [];
|
|
141
|
+
const localImports = [];
|
|
142
|
+
for (const line of importLines) {
|
|
143
|
+
if (fromImportRegex.test(line))
|
|
144
|
+
moduleImports.push(line);
|
|
145
|
+
else
|
|
146
|
+
localImports.push(line);
|
|
147
|
+
}
|
|
148
|
+
moduleImports.sort((a, b) => {
|
|
149
|
+
const am = fromImportRegex.exec(a);
|
|
150
|
+
const bm = fromImportRegex.exec(b);
|
|
151
|
+
const cmp = am[2].localeCompare(bm[2]);
|
|
152
|
+
return cmp !== 0 ? cmp : am[1].localeCompare(bm[1]);
|
|
153
|
+
});
|
|
154
|
+
localImports.sort((a, b) => {
|
|
155
|
+
const ap = a.replace(/^\s*'\s*@import\s+/, '');
|
|
156
|
+
const bp = b.replace(/^\s*'\s*@import\s+/, '');
|
|
157
|
+
return ap.localeCompare(bp);
|
|
158
|
+
});
|
|
159
|
+
const fromMockRegex = /^\s*'\s*@mock\s+(.*?)\s+from\s+(\S+)\s*$/;
|
|
160
|
+
const moduleMocks = [];
|
|
161
|
+
const localMocks = [];
|
|
162
|
+
for (const line of mockLines) {
|
|
163
|
+
if (fromMockRegex.test(line))
|
|
164
|
+
moduleMocks.push(line);
|
|
165
|
+
else
|
|
166
|
+
localMocks.push(line);
|
|
167
|
+
}
|
|
168
|
+
moduleMocks.sort((a, b) => {
|
|
169
|
+
const am = fromMockRegex.exec(a);
|
|
170
|
+
const bm = fromMockRegex.exec(b);
|
|
171
|
+
const cmp = am[2].localeCompare(bm[2]);
|
|
172
|
+
return cmp !== 0 ? cmp : am[1].localeCompare(bm[1]);
|
|
173
|
+
});
|
|
174
|
+
localMocks.sort((a, b) => {
|
|
175
|
+
const ap = a.replace(/^\s*'\s*@mock\s+/, '');
|
|
176
|
+
const bp = b.replace(/^\s*'\s*@mock\s+/, '');
|
|
177
|
+
return ap.localeCompare(bp);
|
|
178
|
+
});
|
|
179
|
+
const sorted = [...moduleImports, ...localImports];
|
|
180
|
+
if (mockLines.length > 0) {
|
|
181
|
+
sorted.push(...moduleMocks, ...localMocks);
|
|
182
|
+
}
|
|
183
|
+
if (config.emptyLineAfterImports) {
|
|
184
|
+
const rest = lines.slice(blockEnd);
|
|
185
|
+
if (rest.length > 0 && rest[0].trim() !== '') {
|
|
186
|
+
sorted.push('');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return [...sorted, ...lines.slice(blockEnd)];
|
|
190
|
+
}
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Pass 2 — Comment normalization
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
function passCommentNormalization(lines, config) {
|
|
195
|
+
if (config.commentStyle === 'preserve' && !config.spaceAfterCommentMarker)
|
|
196
|
+
return lines;
|
|
197
|
+
return lines.map(line => {
|
|
198
|
+
const trimmed = line.trim();
|
|
199
|
+
if (/^\s*'\s*@(?:import|mock)\s+/.test(line))
|
|
200
|
+
return line;
|
|
201
|
+
const isTickComment = trimmed.startsWith("'");
|
|
202
|
+
const isRemComment = /^rem\b/i.test(trimmed);
|
|
203
|
+
if (!isTickComment && !isRemComment)
|
|
204
|
+
return line;
|
|
205
|
+
const indent = line.match(/^(\s*)/)?.[1] ?? '';
|
|
206
|
+
let content = isTickComment ? trimmed.slice(1) : trimmed.slice(3);
|
|
207
|
+
let marker;
|
|
208
|
+
if (config.commentStyle === 'preserve') {
|
|
209
|
+
marker = isTickComment ? "'" : trimmed.slice(0, 3);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
marker = config.commentStyle === "'" ? "'" : 'rem';
|
|
213
|
+
}
|
|
214
|
+
if (config.spaceAfterCommentMarker && content.length > 0 && !content.startsWith(' ')) {
|
|
215
|
+
content = ' ' + content;
|
|
216
|
+
}
|
|
217
|
+
return indent + marker + content;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Pass 3 — End keyword style
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
function passEndKeywordStyle(lines, config) {
|
|
224
|
+
if (config.endKeywordStyle === 'preserve')
|
|
225
|
+
return lines;
|
|
226
|
+
return lines.map(line => {
|
|
227
|
+
const lower = line.trim().toLowerCase();
|
|
228
|
+
const indent = line.match(/^(\s*)/)?.[1] ?? '';
|
|
229
|
+
if (config.endKeywordStyle === 'compact') {
|
|
230
|
+
const compact = SPACED_TO_COMPACT[lower];
|
|
231
|
+
if (compact)
|
|
232
|
+
return indent + compact;
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
const spaced = COMPACT_TO_SPACED[lower];
|
|
236
|
+
if (spaced)
|
|
237
|
+
return indent + spaced;
|
|
238
|
+
}
|
|
239
|
+
return line;
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
// Pass 3b — function vs sub for void
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
function passFunctionVsSub(lines, config) {
|
|
246
|
+
if (config.functionVsSubForVoid === 'preserve')
|
|
247
|
+
return lines;
|
|
248
|
+
const result = [...lines];
|
|
249
|
+
const declRegex = /^(\s*)(function|sub)\s+(\w+)\s*\(([^)]*)\)(?:\s+as\s+(\w+))?\s*$/i;
|
|
250
|
+
for (let i = 0; i < result.length; i++) {
|
|
251
|
+
const m = declRegex.exec(result[i]);
|
|
252
|
+
if (!m)
|
|
253
|
+
continue;
|
|
254
|
+
const [, indent, keyword, name, params, returnType] = m;
|
|
255
|
+
const kw = keyword.toLowerCase();
|
|
256
|
+
const endIdx = findMatchingEnd(result, i);
|
|
257
|
+
if (endIdx < 0)
|
|
258
|
+
continue;
|
|
259
|
+
const hasExplicitReturnType = returnType && returnType.toLowerCase() !== 'void';
|
|
260
|
+
const hasValueReturn = hasReturnWithValue(result, i + 1, endIdx);
|
|
261
|
+
const isVoid = !hasExplicitReturnType && !hasValueReturn;
|
|
262
|
+
if (config.functionVsSubForVoid === 'sub' && kw === 'function' && isVoid) {
|
|
263
|
+
result[i] = `${indent}sub ${name}(${params})`;
|
|
264
|
+
const ei = result[endIdx].match(/^(\s*)/)?.[1] ?? '';
|
|
265
|
+
const el = result[endIdx].trim().toLowerCase();
|
|
266
|
+
if (el === 'end function')
|
|
267
|
+
result[endIdx] = ei + 'end sub';
|
|
268
|
+
else if (el === 'endfunction')
|
|
269
|
+
result[endIdx] = ei + 'endsub';
|
|
270
|
+
}
|
|
271
|
+
else if (config.functionVsSubForVoid === 'function' && kw === 'sub') {
|
|
272
|
+
result[i] = `${indent}function ${name}(${params}) as Void`;
|
|
273
|
+
const ei = result[endIdx].match(/^(\s*)/)?.[1] ?? '';
|
|
274
|
+
const el = result[endIdx].trim().toLowerCase();
|
|
275
|
+
if (el === 'end sub')
|
|
276
|
+
result[endIdx] = ei + 'end function';
|
|
277
|
+
else if (el === 'endsub')
|
|
278
|
+
result[endIdx] = ei + 'endfunction';
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
function hasReturnWithValue(lines, startIdx, endIdx) {
|
|
284
|
+
let depth = 0;
|
|
285
|
+
for (let i = startIdx; i < endIdx; i++) {
|
|
286
|
+
const trimmed = lines[i].trim().toLowerCase();
|
|
287
|
+
if (/^(?:function|sub)\b/.test(trimmed))
|
|
288
|
+
depth++;
|
|
289
|
+
else if (/^(?:end\s*function|end\s*sub|endfunction|endsub)\b/.test(trimmed))
|
|
290
|
+
depth--;
|
|
291
|
+
if (depth > 0)
|
|
292
|
+
continue;
|
|
293
|
+
if (/^return\s+\S/i.test(trimmed))
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
function findMatchingEnd(lines, startIdx) {
|
|
299
|
+
let depth = 1;
|
|
300
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
301
|
+
const lower = lines[i].trim().toLowerCase();
|
|
302
|
+
if (/^(?:function|sub)\b/i.test(lower))
|
|
303
|
+
depth++;
|
|
304
|
+
else if (/^(?:end\s*function|end\s*sub|endfunction|endsub)\b/i.test(lower)) {
|
|
305
|
+
if (--depth === 0)
|
|
306
|
+
return i;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return -1;
|
|
310
|
+
}
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
// Pass 4 — Then style
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
function passThenStyle(lines, config) {
|
|
315
|
+
if (config.thenStyle === 'preserve')
|
|
316
|
+
return lines;
|
|
317
|
+
return lines.map(line => {
|
|
318
|
+
const trimmed = line.trim();
|
|
319
|
+
if (!/^(?:if|else\s*if|elseif)\b/i.test(trimmed))
|
|
320
|
+
return line;
|
|
321
|
+
const indent = line.match(/^(\s*)/)?.[1] ?? '';
|
|
322
|
+
const hasThen = /\bthen\b/i.test(trimmed);
|
|
323
|
+
let isSingleLine = false;
|
|
324
|
+
if (hasThen) {
|
|
325
|
+
const afterThen = trimmed.replace(/^.*?\bthen\b/i, '').trim();
|
|
326
|
+
isSingleLine = afterThen !== '' && !afterThen.startsWith("'") && !/^rem\b/i.test(afterThen);
|
|
327
|
+
}
|
|
328
|
+
const removeThen = () => {
|
|
329
|
+
const cleaned = trimmed.replace(/\s*\bthen\b\s*(?=(?:'.*)?$)/i, '').trimEnd();
|
|
330
|
+
const trailingComment = trimmed.match(/\bthen\b\s*('.*)/i);
|
|
331
|
+
return indent + cleaned + (trailingComment ? ' ' + trailingComment[1] : '');
|
|
332
|
+
};
|
|
333
|
+
switch (config.thenStyle) {
|
|
334
|
+
case 'always':
|
|
335
|
+
if (!hasThen)
|
|
336
|
+
return indent + trimmed + ' then';
|
|
337
|
+
break;
|
|
338
|
+
case 'never':
|
|
339
|
+
if (hasThen && !isSingleLine)
|
|
340
|
+
return removeThen();
|
|
341
|
+
break;
|
|
342
|
+
case 'multiline-only':
|
|
343
|
+
if (!hasThen)
|
|
344
|
+
return indent + trimmed + ' then';
|
|
345
|
+
break;
|
|
346
|
+
case 'singleline-only':
|
|
347
|
+
if (hasThen && !isSingleLine)
|
|
348
|
+
return removeThen();
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
return line;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
// Pass 4b — Parenthesis if case
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
function passParenthesisIfCase(lines, config) {
|
|
358
|
+
if (config.parenthesisIfCase === 'preserve')
|
|
359
|
+
return lines;
|
|
360
|
+
return lines.map(line => {
|
|
361
|
+
const trimmed = line.trim();
|
|
362
|
+
if (!/^(?:if|else\s*if|elseif)\b/i.test(trimmed))
|
|
363
|
+
return line;
|
|
364
|
+
const indent = line.match(/^(\s*)/)?.[1] ?? '';
|
|
365
|
+
if (config.parenthesisIfCase === 'always') {
|
|
366
|
+
const mThen = /^((?:if|else\s*if|elseif)\b)\s+(.+?)\s+(then\s*(?:'.*)?)\s*$/i.exec(trimmed);
|
|
367
|
+
if (mThen) {
|
|
368
|
+
let condition = mThen[2];
|
|
369
|
+
if (!isWrappedInParens(condition)) {
|
|
370
|
+
condition = `(${condition})`;
|
|
371
|
+
}
|
|
372
|
+
return indent + mThen[1] + ' ' + condition + ' ' + mThen[3];
|
|
373
|
+
}
|
|
374
|
+
const mNoThen = /^((?:if|else\s*if|elseif)\b)\s+(.+?)\s*$/i.exec(trimmed);
|
|
375
|
+
if (mNoThen) {
|
|
376
|
+
const rest = mNoThen[2];
|
|
377
|
+
const hasThen = /\bthen\b/i.test(rest);
|
|
378
|
+
if (!hasThen) {
|
|
379
|
+
if (isWrappedInParens(rest)) {
|
|
380
|
+
return line;
|
|
381
|
+
}
|
|
382
|
+
if (/\breturn\b/i.test(rest) || /=\s*\S/.test(rest.replace(/[<>!]=?|<>/g, ''))) {
|
|
383
|
+
return line;
|
|
384
|
+
}
|
|
385
|
+
return indent + mNoThen[1] + ' (' + rest + ')';
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
const m = /^((?:if|else\s*if|elseif)\b)\s+\((.+)\)(.*?)$/i.exec(trimmed);
|
|
391
|
+
if (m && isWrappedInParens('(' + m[2] + ')')) {
|
|
392
|
+
return indent + m[1] + ' ' + m[2] + m[3];
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return line;
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
function isWrappedInParens(s) {
|
|
399
|
+
if (!s.startsWith('(') || !s.endsWith(')'))
|
|
400
|
+
return false;
|
|
401
|
+
let depth = 0;
|
|
402
|
+
for (let i = 0; i < s.length; i++) {
|
|
403
|
+
if (s[i] === '(')
|
|
404
|
+
depth++;
|
|
405
|
+
else if (s[i] === ')')
|
|
406
|
+
depth--;
|
|
407
|
+
if (depth === 0 && i < s.length - 1)
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
return depth === 0;
|
|
411
|
+
}
|
|
412
|
+
// ---------------------------------------------------------------------------
|
|
413
|
+
// Pass 5 — Print statement handling
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
function passPrintStatement(lines, config) {
|
|
416
|
+
if (config.printStatement !== 'remove')
|
|
417
|
+
return lines;
|
|
418
|
+
return lines.filter(line => !/^\s*(?:print|\?)\b/i.test(line));
|
|
419
|
+
}
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
// Pass 6 — Spacing rules
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
423
|
+
function passSpacing(lines, config) {
|
|
424
|
+
return lines.map(line => {
|
|
425
|
+
const trimmed = line.trim();
|
|
426
|
+
if (trimmed === '' || trimmed.startsWith("'") || /^rem\b/i.test(trimmed))
|
|
427
|
+
return line;
|
|
428
|
+
const indent = line.match(/^(\s*)/)?.[1] ?? '';
|
|
429
|
+
const segments = splitCodeSegments(trimmed);
|
|
430
|
+
let result = '';
|
|
431
|
+
for (const seg of segments) {
|
|
432
|
+
if (seg.isCode) {
|
|
433
|
+
result += applySpacingToCode(seg.text, config, trimmed);
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
result += seg.text;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return indent + result;
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
function applySpacingToCode(code, config, fullLine) {
|
|
443
|
+
let r = code;
|
|
444
|
+
if (config.spaceAroundOperators) {
|
|
445
|
+
r = r.replace(/(\S)\s*([+\-*/\\])=\s*(\S)/g, '$1 $2= $3');
|
|
446
|
+
r = r.replace(/(\S)\s*([+\-*/\\])=$/g, '$1 $2=');
|
|
447
|
+
r = r.replace(/([^\s<>])(<>|<=|>=|<<|>>)/g, '$1 $2');
|
|
448
|
+
r = r.replace(/(<>|<=|>=|<<|>>)([^\s<>=])/g, '$1 $2');
|
|
449
|
+
r = r.replace(/(\S)\s*([+*/\\])(?!=)\s*(\S)/g, (_, pre, op, post) => {
|
|
450
|
+
return pre + ' ' + op + ' ' + post;
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
if (config.spaceAroundAssignment) {
|
|
454
|
+
r = r.replace(/([^\s<>!=+\-*/\\])=/g, '$1 =');
|
|
455
|
+
r = r.replace(/=([^\s<>!=])/g, '= $1');
|
|
456
|
+
}
|
|
457
|
+
if (config.unarySpacing) {
|
|
458
|
+
r = r.replace(/\bnot\b(?=\S)/gi, 'not ');
|
|
459
|
+
}
|
|
460
|
+
if (config.forLoopSpacing && /^for\b/i.test(fullLine.trim())) {
|
|
461
|
+
r = r.replace(/(\S)\s*\b(to|step)\b\s*(\S)/gi, '$1 $2 $3');
|
|
462
|
+
}
|
|
463
|
+
if (/^\s*(?:function|sub)\s+\w+/i.test(fullLine)) {
|
|
464
|
+
if (config.spaceBeforeNamedFunctionParens) {
|
|
465
|
+
r = r.replace(/(\b(?:function|sub)\s+\w+)\(/i, '$1 (');
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
r = r.replace(/(\b(?:function|sub)\s+\w+)\s+\(/i, '$1(');
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (/=\s*(?:function|sub)\s*\(/i.test(r)) {
|
|
472
|
+
if (config.spaceBeforeAnonymousFunctionParens) {
|
|
473
|
+
r = r.replace(/(=\s*(?:function|sub))\s*\(/gi, '$1 (');
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
r = r.replace(/(=\s*(?:function|sub))\s+\(/gi, '$1(');
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const _parenSkipWords = /^(?:if|for|while|print|and|or|not|mod|return|then|else|elseif|to|step|in|each|as|dim|end|exit)$/i;
|
|
480
|
+
if (config.spaceBeforeCallParens) {
|
|
481
|
+
r = r.replace(/(\b(?!function\b|sub\b)[a-zA-Z_]\w*)\(/gi, (match, name) => {
|
|
482
|
+
if (_parenSkipWords.test(name))
|
|
483
|
+
return match;
|
|
484
|
+
return name + ' (';
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
r = r.replace(/(\b(?!function\b|sub\b)[a-zA-Z_]\w*)\s+\(/gi, (match, name) => {
|
|
489
|
+
if (_parenSkipWords.test(name))
|
|
490
|
+
return match;
|
|
491
|
+
return name + '(';
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
if (config.spaceInsideParens === 'always') {
|
|
495
|
+
r = r.replace(/\(([^\s)][^)]*)\)/g, '( $1 )');
|
|
496
|
+
}
|
|
497
|
+
else if (config.spaceInsideParens === 'never') {
|
|
498
|
+
r = r.replace(/\(\s+/g, '(');
|
|
499
|
+
r = r.replace(/\s+\)/g, ')');
|
|
500
|
+
}
|
|
501
|
+
if (config.bracketSpacing) {
|
|
502
|
+
r = r.replace(/\{([^\s}])/g, '{ $1');
|
|
503
|
+
r = r.replace(/([^\s{])\}/g, '$1 }');
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
r = r.replace(/\{\s+/g, '{');
|
|
507
|
+
r = r.replace(/\s+\}/g, '}');
|
|
508
|
+
}
|
|
509
|
+
return r;
|
|
510
|
+
}
|
|
511
|
+
// ---------------------------------------------------------------------------
|
|
512
|
+
// Pass 7 — Casing
|
|
513
|
+
// ---------------------------------------------------------------------------
|
|
514
|
+
function passCasing(lines, casing, userFuncMap) {
|
|
515
|
+
return lines.map(line => {
|
|
516
|
+
const trimmed = line.trim();
|
|
517
|
+
if (trimmed === '' || trimmed.startsWith("'") || /^rem\b/i.test(trimmed))
|
|
518
|
+
return line;
|
|
519
|
+
const indent = line.match(/^(\s*)/)?.[1] ?? '';
|
|
520
|
+
return indent + applyCasingToLine(trimmed, casing, userFuncMap);
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
// ---------------------------------------------------------------------------
|
|
524
|
+
// Pass 7b — Split array open bracket
|
|
525
|
+
// ---------------------------------------------------------------------------
|
|
526
|
+
function passSplitArrayOpenBracket(lines, config) {
|
|
527
|
+
if (!config.splitArrayOpenBracket)
|
|
528
|
+
return lines;
|
|
529
|
+
const result = [];
|
|
530
|
+
for (let i = 0; i < lines.length; i++) {
|
|
531
|
+
const line = lines[i];
|
|
532
|
+
const trimmed = line.trim();
|
|
533
|
+
if (/\[\{\s*$/.test(trimmed)) {
|
|
534
|
+
let hasMultipleItems = false;
|
|
535
|
+
let depth = 0;
|
|
536
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
537
|
+
const t = lines[j].trim();
|
|
538
|
+
for (const ch of t) {
|
|
539
|
+
if (ch === '{' || ch === '[')
|
|
540
|
+
depth++;
|
|
541
|
+
else if (ch === '}' || ch === ']')
|
|
542
|
+
depth--;
|
|
543
|
+
}
|
|
544
|
+
if (/^},/.test(t) && depth <= 0) {
|
|
545
|
+
hasMultipleItems = true;
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
if (/^\]/.test(t) && depth < 0)
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
if (hasMultipleItems) {
|
|
552
|
+
const indent = line.match(/^(\s*)/)?.[1] ?? '';
|
|
553
|
+
const prefix = trimmed.slice(0, -1);
|
|
554
|
+
result.push(indent + prefix);
|
|
555
|
+
result.push(indent + ' {');
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
result.push(line);
|
|
560
|
+
}
|
|
561
|
+
return result;
|
|
562
|
+
}
|
|
563
|
+
// ---------------------------------------------------------------------------
|
|
564
|
+
// Pass 8b — Comma handling (trailing commas + array/AA comma style)
|
|
565
|
+
// ---------------------------------------------------------------------------
|
|
566
|
+
function passTrailingCommas(lines, config) {
|
|
567
|
+
const result = [...lines];
|
|
568
|
+
for (let i = 0; i < result.length; i++) {
|
|
569
|
+
const trimmed = result[i].trim();
|
|
570
|
+
if (!/^\s*[}\]]/.test(result[i]))
|
|
571
|
+
continue;
|
|
572
|
+
const closerChar = trimmed[0];
|
|
573
|
+
const isArray = closerChar === ']';
|
|
574
|
+
const isAA = closerChar === '}';
|
|
575
|
+
const itemStyle = isArray ? config.arrayCommaStyle : isAA ? config.assocArrayCommaStyle : 'preserve';
|
|
576
|
+
const openerChar = isArray ? '[' : '{';
|
|
577
|
+
let depth = 0;
|
|
578
|
+
for (let j = i; j >= 0; j--) {
|
|
579
|
+
const jTrimmed = result[j].trim();
|
|
580
|
+
for (let k = jTrimmed.length - 1; k >= 0; k--) {
|
|
581
|
+
if (jTrimmed[k] === closerChar)
|
|
582
|
+
depth++;
|
|
583
|
+
else if (jTrimmed[k] === openerChar)
|
|
584
|
+
depth--;
|
|
585
|
+
}
|
|
586
|
+
if (depth <= 0)
|
|
587
|
+
break;
|
|
588
|
+
if (j < i && j > 0 && jTrimmed !== '' && !jTrimmed.startsWith("'") && !/^rem\b/i.test(jTrimmed)) {
|
|
589
|
+
if (/\b(?:function|sub)\s*\([^)]*\)(?:\s+as\s+\w+)?\s*$/i.test(jTrimmed))
|
|
590
|
+
continue;
|
|
591
|
+
if (jTrimmed === '{' || jTrimmed === '[')
|
|
592
|
+
continue;
|
|
593
|
+
if (/^return\b/i.test(jTrimmed))
|
|
594
|
+
continue;
|
|
595
|
+
if (/^(?:if|else|elseif|else\s+if|end\s*if|end\s*sub|end\s*function|end\s*for|end\s*while|end\s*try|for|while|try|catch|next|exit|throw|dim|print|\?)\b/i.test(jTrimmed))
|
|
596
|
+
continue;
|
|
597
|
+
if (isAA && !/^\w+\s*:/.test(jTrimmed) && !/^"[^"]*"\s*:/.test(jTrimmed))
|
|
598
|
+
continue;
|
|
599
|
+
const isLastItem = j === i - 1 || (() => {
|
|
600
|
+
for (let k = j + 1; k < i; k++) {
|
|
601
|
+
if (result[k].trim() !== '')
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
return true;
|
|
605
|
+
})();
|
|
606
|
+
const codeOnly = stripTrailingComment(jTrimmed);
|
|
607
|
+
const alreadyHasComma = codeOnly.endsWith(',');
|
|
608
|
+
if (isLastItem) {
|
|
609
|
+
if (config.trailingComma === 'always' || config.trailingComma === 'multiline') {
|
|
610
|
+
if (!alreadyHasComma && jTrimmed !== openerChar) {
|
|
611
|
+
result[j] = insertCommaBeforeComment(result[j]);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
else if (config.trailingComma === 'never') {
|
|
615
|
+
if (alreadyHasComma) {
|
|
616
|
+
result[j] = removeCommaBeforeComment(result[j]);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
if (itemStyle === 'always') {
|
|
622
|
+
if (!alreadyHasComma && !codeOnly.endsWith('{') && !codeOnly.endsWith('[')) {
|
|
623
|
+
result[j] = insertCommaBeforeComment(result[j]);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
else if (itemStyle === 'never') {
|
|
627
|
+
if (alreadyHasComma) {
|
|
628
|
+
result[j] = removeCommaBeforeComment(result[j]);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return result;
|
|
636
|
+
}
|
|
637
|
+
function stripTrailingComment(trimmed) {
|
|
638
|
+
let inStr = false;
|
|
639
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
640
|
+
if (trimmed[i] === '"')
|
|
641
|
+
inStr = !inStr;
|
|
642
|
+
else if (trimmed[i] === "'" && !inStr) {
|
|
643
|
+
return trimmed.substring(0, i).trimEnd();
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return trimmed;
|
|
647
|
+
}
|
|
648
|
+
function insertCommaBeforeComment(line) {
|
|
649
|
+
let inStr = false;
|
|
650
|
+
for (let i = 0; i < line.length; i++) {
|
|
651
|
+
if (line[i] === '"')
|
|
652
|
+
inStr = !inStr;
|
|
653
|
+
else if (line[i] === "'" && !inStr) {
|
|
654
|
+
const before = line.substring(0, i).trimEnd();
|
|
655
|
+
const after = line.substring(i);
|
|
656
|
+
return before + ', ' + after;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return line.trimEnd() + ',';
|
|
660
|
+
}
|
|
661
|
+
function removeCommaBeforeComment(line) {
|
|
662
|
+
let inStr = false;
|
|
663
|
+
for (let i = 0; i < line.length; i++) {
|
|
664
|
+
if (line[i] === '"')
|
|
665
|
+
inStr = !inStr;
|
|
666
|
+
else if (line[i] === "'" && !inStr) {
|
|
667
|
+
const before = line.substring(0, i).trimEnd();
|
|
668
|
+
const after = line.substring(i);
|
|
669
|
+
return before.replace(/,\s*$/, '') + ' ' + after;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return line.replace(/,(\s*)$/, '$1');
|
|
673
|
+
}
|
|
674
|
+
// ---------------------------------------------------------------------------
|
|
675
|
+
// Pass 8 — Indentation
|
|
676
|
+
// ---------------------------------------------------------------------------
|
|
677
|
+
function passIndentation(lines, config) {
|
|
678
|
+
const indentUnit = config.useTabs ? '\t' : ' '.repeat(config.indentSize);
|
|
679
|
+
let indentLevel = 0;
|
|
680
|
+
return lines.map(line => {
|
|
681
|
+
const trimmed = line.trim();
|
|
682
|
+
if (trimmed === '')
|
|
683
|
+
return '';
|
|
684
|
+
if (isDeindentLine(trimmed) && indentLevel > 0)
|
|
685
|
+
indentLevel--;
|
|
686
|
+
if (/^[}\]]/.test(trimmed) && !isDeindentLine(trimmed)) {
|
|
687
|
+
let closers = 0;
|
|
688
|
+
for (const ch of trimmed) {
|
|
689
|
+
if (ch === '}' || ch === ']')
|
|
690
|
+
closers++;
|
|
691
|
+
else
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
indentLevel = Math.max(0, indentLevel - closers);
|
|
695
|
+
}
|
|
696
|
+
const result = indentUnit.repeat(indentLevel) + trimmed;
|
|
697
|
+
if (isIndentLine(trimmed))
|
|
698
|
+
indentLevel++;
|
|
699
|
+
if (!/^[}\]]/.test(trimmed)) {
|
|
700
|
+
let opens = 0;
|
|
701
|
+
let depth = 0;
|
|
702
|
+
for (const ch of trimmed) {
|
|
703
|
+
if (ch === '{' || ch === '[') {
|
|
704
|
+
depth++;
|
|
705
|
+
opens++;
|
|
706
|
+
}
|
|
707
|
+
else if (ch === '}' || ch === ']') {
|
|
708
|
+
depth--;
|
|
709
|
+
opens = Math.max(0, opens - 1);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
if (depth > 0)
|
|
713
|
+
indentLevel += depth;
|
|
714
|
+
}
|
|
715
|
+
if (/\b(?:function|sub)\s*\([^)]*\)(?:\s+as\s+\w+)?\s*$/i.test(trimmed) && !/^(?:function|sub)\b/i.test(trimmed)) {
|
|
716
|
+
indentLevel++;
|
|
717
|
+
}
|
|
718
|
+
return result;
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
// ---------------------------------------------------------------------------
|
|
722
|
+
// Pass 9 — Blank line rules
|
|
723
|
+
// ---------------------------------------------------------------------------
|
|
724
|
+
function passBlankLines(lines, config) {
|
|
725
|
+
let result = [...lines];
|
|
726
|
+
if (config.maxEmptyLines > 0) {
|
|
727
|
+
const filtered = [];
|
|
728
|
+
let consecutive = 0;
|
|
729
|
+
for (const line of result) {
|
|
730
|
+
if (line.trim() === '') {
|
|
731
|
+
consecutive++;
|
|
732
|
+
if (consecutive <= config.maxEmptyLines)
|
|
733
|
+
filtered.push(line);
|
|
734
|
+
}
|
|
735
|
+
else {
|
|
736
|
+
consecutive = 0;
|
|
737
|
+
filtered.push(line);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
result = filtered;
|
|
741
|
+
}
|
|
742
|
+
if (config.emptyLinesBetweenFunctions > 0) {
|
|
743
|
+
const out = [];
|
|
744
|
+
let prevWasEndFuncOrSub = false;
|
|
745
|
+
for (let i = 0; i < result.length; i++) {
|
|
746
|
+
const trimmed = result[i].trim().toLowerCase();
|
|
747
|
+
const isFuncStart = /^(?:function|sub)\b/i.test(trimmed);
|
|
748
|
+
if (isFuncStart && prevWasEndFuncOrSub) {
|
|
749
|
+
while (out.length > 0 && out[out.length - 1].trim() === '')
|
|
750
|
+
out.pop();
|
|
751
|
+
for (let n = 0; n < config.emptyLinesBetweenFunctions; n++)
|
|
752
|
+
out.push('');
|
|
753
|
+
}
|
|
754
|
+
out.push(result[i]);
|
|
755
|
+
prevWasEndFuncOrSub = /^(?:end\s*function|end\s*sub|endfunction|endsub)\b/i.test(trimmed);
|
|
756
|
+
}
|
|
757
|
+
result = out;
|
|
758
|
+
}
|
|
759
|
+
if (config.blankLineAfterFunctionOpen) {
|
|
760
|
+
const out = [];
|
|
761
|
+
for (let i = 0; i < result.length; i++) {
|
|
762
|
+
out.push(result[i]);
|
|
763
|
+
if (/^\s*(?:function|sub)\b/i.test(result[i])) {
|
|
764
|
+
if (i + 1 < result.length && result[i + 1].trim() !== '')
|
|
765
|
+
out.push('');
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
result = out;
|
|
769
|
+
}
|
|
770
|
+
if (config.blankLineBeforeFunctionClose) {
|
|
771
|
+
const out = [];
|
|
772
|
+
for (let i = 0; i < result.length; i++) {
|
|
773
|
+
if (/^\s*(?:end\s*function|end\s*sub|endfunction|endsub)\b/i.test(result[i])) {
|
|
774
|
+
if (out.length > 0 && out[out.length - 1].trim() !== '')
|
|
775
|
+
out.push('');
|
|
776
|
+
}
|
|
777
|
+
out.push(result[i]);
|
|
778
|
+
}
|
|
779
|
+
result = out;
|
|
780
|
+
}
|
|
781
|
+
if (config.blankLineBeforeReturn) {
|
|
782
|
+
const out = [];
|
|
783
|
+
for (let i = 0; i < result.length; i++) {
|
|
784
|
+
if (/^\s*return\b/i.test(result[i])) {
|
|
785
|
+
const isAlone = isReturnAloneInBlock(result, i);
|
|
786
|
+
const shouldAdd = config.blankLineBeforeReturn === 'always' ||
|
|
787
|
+
(config.blankLineBeforeReturn === 'not-alone' && !isAlone);
|
|
788
|
+
if (shouldAdd && out.length > 0 && out[out.length - 1].trim() !== '') {
|
|
789
|
+
out.push('');
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
out.push(result[i]);
|
|
793
|
+
}
|
|
794
|
+
result = out;
|
|
795
|
+
}
|
|
796
|
+
if (config.blankLineBeforeComment) {
|
|
797
|
+
const out = [];
|
|
798
|
+
for (let i = 0; i < result.length; i++) {
|
|
799
|
+
const trimmed = result[i].trim();
|
|
800
|
+
const isComment = trimmed.startsWith("'") || /^rem\b/i.test(trimmed);
|
|
801
|
+
if (isComment && out.length > 0 && out[out.length - 1].trim() !== '') {
|
|
802
|
+
if (!/^\s*'\s*@(?:import|mock)\s+/.test(result[i]))
|
|
803
|
+
out.push('');
|
|
804
|
+
}
|
|
805
|
+
out.push(result[i]);
|
|
806
|
+
}
|
|
807
|
+
result = out;
|
|
808
|
+
}
|
|
809
|
+
if (config.emptyLinesAtBlockBoundaries === 'strip') {
|
|
810
|
+
const blockOpeners = /^\s*(?:function|sub|if\b.*\bthen\s*$|else\b|elseif\b|for\b|while\b|try\b|catch\b)/i;
|
|
811
|
+
const blockClosers = /^\s*(?:end\s*function|end\s*sub|end\s*if|end\s*for|end\s*while|end\s*try|endif|endfunction|endsub|endfor|endwhile|endtry|next|else|elseif|catch)\b/i;
|
|
812
|
+
const out = [];
|
|
813
|
+
for (let i = 0; i < result.length; i++) {
|
|
814
|
+
if (result[i].trim() === '') {
|
|
815
|
+
if (i > 0 && blockOpeners.test(result[i - 1]))
|
|
816
|
+
continue;
|
|
817
|
+
if (i + 1 < result.length && blockClosers.test(result[i + 1]))
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
out.push(result[i]);
|
|
821
|
+
}
|
|
822
|
+
result = out;
|
|
823
|
+
}
|
|
824
|
+
return result;
|
|
825
|
+
}
|
|
826
|
+
// ---------------------------------------------------------------------------
|
|
827
|
+
// Pass 10 — Trim trailing whitespace
|
|
828
|
+
// ---------------------------------------------------------------------------
|
|
829
|
+
function passTrimTrailing(lines, config) {
|
|
830
|
+
if (!config.trimTrailingWhitespace)
|
|
831
|
+
return lines;
|
|
832
|
+
return lines.map(line => line.trimEnd());
|
|
833
|
+
}
|
|
834
|
+
// ---------------------------------------------------------------------------
|
|
835
|
+
// Pass 11 — Comment width
|
|
836
|
+
// ---------------------------------------------------------------------------
|
|
837
|
+
function passCommentWidth(lines, config) {
|
|
838
|
+
if (config.commentWidth <= 0)
|
|
839
|
+
return lines;
|
|
840
|
+
const result = [];
|
|
841
|
+
for (const line of lines) {
|
|
842
|
+
const trimmed = line.trim();
|
|
843
|
+
const isTickComment = trimmed.startsWith("'");
|
|
844
|
+
const isRemComment = /^rem\b/i.test(trimmed);
|
|
845
|
+
if (!isTickComment && !isRemComment) {
|
|
846
|
+
result.push(line);
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
if (line.length <= config.commentWidth) {
|
|
850
|
+
result.push(line);
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
const indent = line.match(/^(\s*)/)?.[1] ?? '';
|
|
854
|
+
const marker = isTickComment ? "' " : 'rem ';
|
|
855
|
+
const content = isTickComment ? trimmed.slice(1).trim() : trimmed.slice(3).trim();
|
|
856
|
+
const prefix = indent + marker;
|
|
857
|
+
const maxContent = config.commentWidth - prefix.length;
|
|
858
|
+
if (maxContent <= 0) {
|
|
859
|
+
result.push(line);
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
let remaining = content;
|
|
863
|
+
while (remaining.length > 0) {
|
|
864
|
+
if (remaining.length <= maxContent) {
|
|
865
|
+
result.push(prefix + remaining);
|
|
866
|
+
break;
|
|
867
|
+
}
|
|
868
|
+
let breakAt = remaining.lastIndexOf(' ', maxContent);
|
|
869
|
+
if (breakAt <= 0)
|
|
870
|
+
breakAt = maxContent;
|
|
871
|
+
result.push(prefix + remaining.slice(0, breakAt).trimEnd());
|
|
872
|
+
remaining = remaining.slice(breakAt).trimStart();
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return result;
|
|
876
|
+
}
|
|
877
|
+
// ---------------------------------------------------------------------------
|
|
878
|
+
// Indent tracking helpers
|
|
879
|
+
// ---------------------------------------------------------------------------
|
|
880
|
+
function isIndentLine(trimmed) {
|
|
881
|
+
const lower = trimmed.toLowerCase();
|
|
882
|
+
if (/^(?:if|else\s*if|elseif)\b/i.test(lower)) {
|
|
883
|
+
if (/\bthen\b/i.test(lower)) {
|
|
884
|
+
const afterThen = lower.replace(/^.*?\bthen\b/i, '').trim();
|
|
885
|
+
if (afterThen !== '' && !afterThen.startsWith("'") && !/^rem\b/i.test(afterThen)) {
|
|
886
|
+
return false;
|
|
887
|
+
}
|
|
888
|
+
return true;
|
|
889
|
+
}
|
|
890
|
+
if (/^(?:if|else\s*if|elseif)\s+\(/i.test(lower)) {
|
|
891
|
+
let depth = 0;
|
|
892
|
+
let afterParen = -1;
|
|
893
|
+
for (let i = 0; i < lower.length; i++) {
|
|
894
|
+
if (lower[i] === '(')
|
|
895
|
+
depth++;
|
|
896
|
+
else if (lower[i] === ')') {
|
|
897
|
+
depth--;
|
|
898
|
+
if (depth === 0) {
|
|
899
|
+
afterParen = i + 1;
|
|
900
|
+
break;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
if (afterParen > 0) {
|
|
905
|
+
const rest = lower.substring(afterParen).trim();
|
|
906
|
+
if (rest !== '' && !rest.startsWith("'") && !/^rem\b/i.test(rest)) {
|
|
907
|
+
return false;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
return true;
|
|
912
|
+
}
|
|
913
|
+
if (/^(?:function|sub)\b/i.test(lower))
|
|
914
|
+
return true;
|
|
915
|
+
if (/^(?:else|elseif)\b/i.test(lower))
|
|
916
|
+
return true;
|
|
917
|
+
if (/^for\b/i.test(lower))
|
|
918
|
+
return true;
|
|
919
|
+
if (/^while\b/i.test(lower))
|
|
920
|
+
return true;
|
|
921
|
+
if (/^try\b/i.test(lower))
|
|
922
|
+
return true;
|
|
923
|
+
if (/^catch\b/i.test(lower))
|
|
924
|
+
return true;
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
function isDeindentLine(trimmed) {
|
|
928
|
+
const lower = trimmed.toLowerCase();
|
|
929
|
+
if (/^end\s*(function|sub|if|for|while|try)\b/i.test(lower))
|
|
930
|
+
return true;
|
|
931
|
+
if (/^(?:endfunction|endsub|endif|endfor|endwhile|endtry)\b/i.test(lower))
|
|
932
|
+
return true;
|
|
933
|
+
if (/^(?:next|endwhile)\b/i.test(lower))
|
|
934
|
+
return true;
|
|
935
|
+
if (/^(?:else|elseif)\b/i.test(lower))
|
|
936
|
+
return true;
|
|
937
|
+
if (/^catch\b/i.test(lower))
|
|
938
|
+
return true;
|
|
939
|
+
return false;
|
|
940
|
+
}
|
|
941
|
+
function isReturnAloneInBlock(lines, returnIdx) {
|
|
942
|
+
let openerIdx = returnIdx - 1;
|
|
943
|
+
while (openerIdx >= 0) {
|
|
944
|
+
const t = lines[openerIdx].trim();
|
|
945
|
+
if (t !== '' && !t.startsWith("'") && !/^rem\b/i.test(t)) {
|
|
946
|
+
if (isIndentLine(t))
|
|
947
|
+
break;
|
|
948
|
+
return false;
|
|
949
|
+
}
|
|
950
|
+
openerIdx--;
|
|
951
|
+
}
|
|
952
|
+
let closerIdx = returnIdx + 1;
|
|
953
|
+
while (closerIdx < lines.length) {
|
|
954
|
+
const t = lines[closerIdx].trim();
|
|
955
|
+
if (t !== '' && !t.startsWith("'") && !/^rem\b/i.test(t)) {
|
|
956
|
+
if (isDeindentLine(t))
|
|
957
|
+
break;
|
|
958
|
+
return false;
|
|
959
|
+
}
|
|
960
|
+
closerIdx++;
|
|
961
|
+
}
|
|
962
|
+
return true;
|
|
963
|
+
}
|
|
964
|
+
// ---------------------------------------------------------------------------
|
|
965
|
+
// Casing helpers
|
|
966
|
+
// ---------------------------------------------------------------------------
|
|
967
|
+
function applyCasingToLine(line, casing, userFuncMap) {
|
|
968
|
+
const segments = splitCodeSegments(line);
|
|
969
|
+
let result = '';
|
|
970
|
+
for (const seg of segments) {
|
|
971
|
+
if (seg.isCode) {
|
|
972
|
+
result += transformCodeSegment(seg.text, casing, userFuncMap);
|
|
973
|
+
}
|
|
974
|
+
else {
|
|
975
|
+
result += seg.text;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return result;
|
|
979
|
+
}
|
|
980
|
+
function splitCodeSegments(line) {
|
|
981
|
+
const segments = [];
|
|
982
|
+
let current = '';
|
|
983
|
+
let inString = false;
|
|
984
|
+
for (let i = 0; i < line.length; i++) {
|
|
985
|
+
const ch = line[i];
|
|
986
|
+
if (ch === '"') {
|
|
987
|
+
if (!inString) {
|
|
988
|
+
if (current)
|
|
989
|
+
segments.push({ text: current, isCode: true });
|
|
990
|
+
current = '"';
|
|
991
|
+
inString = true;
|
|
992
|
+
}
|
|
993
|
+
else if (line[i + 1] === '"') {
|
|
994
|
+
current += '""';
|
|
995
|
+
i++;
|
|
996
|
+
}
|
|
997
|
+
else {
|
|
998
|
+
current += '"';
|
|
999
|
+
segments.push({ text: current, isCode: false });
|
|
1000
|
+
current = '';
|
|
1001
|
+
inString = false;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
else if (!inString && ch === "'") {
|
|
1005
|
+
if (current)
|
|
1006
|
+
segments.push({ text: current, isCode: true });
|
|
1007
|
+
segments.push({ text: line.slice(i), isCode: false });
|
|
1008
|
+
return segments;
|
|
1009
|
+
}
|
|
1010
|
+
else {
|
|
1011
|
+
current += ch;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
if (current) {
|
|
1015
|
+
segments.push({ text: current, isCode: inString ? false : true });
|
|
1016
|
+
}
|
|
1017
|
+
return segments;
|
|
1018
|
+
}
|
|
1019
|
+
function transformCodeSegment(code, casing, userFuncMap) {
|
|
1020
|
+
const exactMap = casing.exactCasing ?? {};
|
|
1021
|
+
const userFuncCasing = casing.userFunctions ?? 'NoChange';
|
|
1022
|
+
return code.replace(/\b([a-zA-Z_]\w*)\b/g, (match, _group, offset) => {
|
|
1023
|
+
const afterIdx = offset + match.length;
|
|
1024
|
+
const restAfter = code.slice(afterIdx);
|
|
1025
|
+
if (/^\s*:/.test(restAfter))
|
|
1026
|
+
return match;
|
|
1027
|
+
if (offset > 0 && code[offset - 1] === '.')
|
|
1028
|
+
return match;
|
|
1029
|
+
const lower = match.toLowerCase();
|
|
1030
|
+
const exact = Object.prototype.hasOwnProperty.call(exactMap, lower) ? exactMap[lower] : undefined;
|
|
1031
|
+
if (exact !== undefined)
|
|
1032
|
+
return exact;
|
|
1033
|
+
if (_keywordSet.has(lower)) {
|
|
1034
|
+
let category = (0, builtins_1.getKeywordCategory)(lower);
|
|
1035
|
+
if (lower === 'function' && offset >= 3) {
|
|
1036
|
+
const before = code.slice(Math.max(0, offset - 10), offset);
|
|
1037
|
+
if (/\bas\s+$/i.test(before)) {
|
|
1038
|
+
category = 'type';
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
const effectiveCasing = (0, casing_1.resolveKeywordCasing)(category, casing);
|
|
1042
|
+
if (effectiveCasing !== 'NoChange') {
|
|
1043
|
+
return (0, casing_1.applyCasingWithOverrides)(match, effectiveCasing, exactMap);
|
|
1044
|
+
}
|
|
1045
|
+
return match;
|
|
1046
|
+
}
|
|
1047
|
+
if (_builtinMap.has(lower)) {
|
|
1048
|
+
const canonical = _builtinMap.get(lower);
|
|
1049
|
+
if (casing.builtins !== 'NoChange') {
|
|
1050
|
+
return (0, casing_1.applyCasingWithOverrides)(canonical, casing.builtins, exactMap);
|
|
1051
|
+
}
|
|
1052
|
+
return canonical;
|
|
1053
|
+
}
|
|
1054
|
+
if (userFuncMap.has(lower)) {
|
|
1055
|
+
const definitionName = userFuncMap.get(lower);
|
|
1056
|
+
if (userFuncCasing !== 'NoChange') {
|
|
1057
|
+
return (0, casing_1.applyCasing)(definitionName, userFuncCasing);
|
|
1058
|
+
}
|
|
1059
|
+
return definitionName;
|
|
1060
|
+
}
|
|
1061
|
+
return match;
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
//# sourceMappingURL=formatter.js.map
|