nextlua 2.0.0 → 3.0.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/main.js +273 -24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nextlua",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "A luau beautifier and minifier.",
5
5
  "main": "index.js",
6
6
  "bin": {
package/src/main.js CHANGED
@@ -141,6 +141,115 @@ function readLongBracket(input, index) {
141
141
  };
142
142
  }
143
143
 
144
+ function decodeStringToken(token) {
145
+ const quote = token[0];
146
+ if (quote !== '"' && quote !== "'" && quote !== "`") {
147
+ return token;
148
+ }
149
+
150
+ function keepEscape(code) {
151
+ // Only de-uglify printable ASCII; leave control bytes and chars that
152
+ // would need re-escaping (quote/backslash) as their original escapes.
153
+ if (code < 0x20 || code > 0x7e) {
154
+ return false;
155
+ }
156
+ const ch = String.fromCharCode(code);
157
+ return ch !== '"' && ch !== "'" && ch !== "`" && ch !== "\\";
158
+ }
159
+
160
+ // Pass 1: split into units — final text, or a preserved decimal escape
161
+ // (kept as { dec } so padding can be decided once we know what follows).
162
+ const units = [];
163
+ function pushText(text) {
164
+ const last = units[units.length - 1];
165
+ if (last && last.text !== undefined) {
166
+ last.text += text;
167
+ } else {
168
+ units.push({ text });
169
+ }
170
+ }
171
+
172
+ let i = 0;
173
+ while (i < token.length) {
174
+ const ch = token[i];
175
+ if (ch !== "\\") {
176
+ pushText(ch);
177
+ i++;
178
+ continue;
179
+ }
180
+
181
+ const next = token[i + 1];
182
+ const hexMatch = next === "x" && /^[0-9A-Fa-f]{2}/.test(token.slice(i + 2, i + 4))
183
+ ? token.slice(i + 2, i + 4)
184
+ : null;
185
+ if (hexMatch) {
186
+ const code = parseInt(hexMatch, 16);
187
+ if (keepEscape(code)) {
188
+ pushText(String.fromCharCode(code));
189
+ } else {
190
+ // Hex escapes are fixed-width (\xHH), so never ambiguous.
191
+ pushText("\\x" + hexMatch);
192
+ }
193
+ i += 4;
194
+ continue;
195
+ }
196
+
197
+ const decMatch = /^[0-9]{1,3}/.exec(token.slice(i + 1, i + 4));
198
+ if (decMatch) {
199
+ const digits = decMatch[0];
200
+ const code = parseInt(digits, 10);
201
+ i += 1 + digits.length;
202
+ if (code <= 0xff && keepEscape(code)) {
203
+ pushText(String.fromCharCode(code));
204
+ } else {
205
+ units.push({ dec: Math.min(code, 0xff) });
206
+ }
207
+ continue;
208
+ }
209
+
210
+ // Preserve any other escape sequence verbatim (\n, \t, \\, \" ...).
211
+ pushText(ch + (next === undefined ? "" : next));
212
+ i += next === undefined ? 1 : 2;
213
+ }
214
+
215
+ // Pass 2: serialize. A preserved decimal escape only needs zero-padding to
216
+ // 3 digits when the next output char is a digit (which would otherwise
217
+ // merge into a single, possibly >255, escape).
218
+ let result = "";
219
+ for (let u = 0; u < units.length; u++) {
220
+ const unit = units[u];
221
+ if (unit.text !== undefined) {
222
+ result += unit.text;
223
+ continue;
224
+ }
225
+
226
+ const nextUnit = units[u + 1];
227
+ const nextChar = nextUnit && nextUnit.text !== undefined ? nextUnit.text[0] : "";
228
+ result += "\\" + (/[0-9]/.test(nextChar)
229
+ ? String(unit.dec).padStart(3, "0")
230
+ : String(unit.dec));
231
+ }
232
+
233
+ return result;
234
+ }
235
+
236
+ function normalizeNumberToken(token) {
237
+ // Underscore digit separators are stripped (a no-op when none are present).
238
+ const stripped = token.replace(/_/g, "");
239
+
240
+ const hexMatch = /^0[xX]([0-9A-Fa-f]+)$/.exec(stripped);
241
+ if (hexMatch) {
242
+ return BigInt("0x" + hexMatch[1]).toString(10);
243
+ }
244
+
245
+ const binMatch = /^0[bB]([01]+)$/.exec(stripped);
246
+ if (binMatch) {
247
+ return BigInt("0b" + binMatch[1]).toString(10);
248
+ }
249
+
250
+ return stripped;
251
+ }
252
+
144
253
  function tokenize(input) {
145
254
  const tokens = [];
146
255
  let i = 0;
@@ -174,17 +283,26 @@ function tokenize(input) {
174
283
  if (ch === '"' || ch === "'" || ch === "`") {
175
284
  let value = ch;
176
285
  i++;
286
+ let escaped = false;
177
287
 
178
288
  while (i < input.length) {
179
- value += input[i];
180
- if (input[i] === ch && input[i - 1] !== "\\") {
181
- i++;
289
+ const c = input[i];
290
+ value += c;
291
+ i++;
292
+ if (escaped) {
293
+ escaped = false;
294
+ continue;
295
+ }
296
+ if (c === "\\") {
297
+ escaped = true;
298
+ continue;
299
+ }
300
+ if (c === ch) {
182
301
  break;
183
302
  }
184
- i++;
185
303
  }
186
304
 
187
- tokens.push(value);
305
+ tokens.push(decodeStringToken(value));
188
306
  continue;
189
307
  }
190
308
 
@@ -217,7 +335,7 @@ function tokenize(input) {
217
335
  while (end < input.length && /[A-Fa-f0-9_xX.]/.test(input[end])) {
218
336
  end++;
219
337
  }
220
- tokens.push(input.slice(i, end));
338
+ tokens.push(normalizeNumberToken(input.slice(i, end)));
221
339
  i = end;
222
340
  continue;
223
341
  }
@@ -241,6 +359,26 @@ function isLiteral(token) {
241
359
  return /^["'`]/.test(token) || /^\d/.test(token) || token === "..." || token === "true" || token === "false" || token === "nil";
242
360
  }
243
361
 
362
+ const keywordsSpacedBeforeParen = new Set([
363
+ "if",
364
+ "elseif",
365
+ "while",
366
+ "until",
367
+ "return",
368
+ "and",
369
+ "or",
370
+ "not",
371
+ "in"
372
+ ]);
373
+
374
+ function isValueEnd(token) {
375
+ return (isIdentifier(token) && !reservedKeywords.has(token)) ||
376
+ isLiteral(token) ||
377
+ token === ")" ||
378
+ token === "]" ||
379
+ token === "}";
380
+ }
381
+
244
382
  function canEndStatement(token) {
245
383
  return (isIdentifier(token) && !reservedKeywords.has(token)) || isLiteral(token) || token === ")" || token === "]" || token === "}" || token === "end" || token === "break" || token === "continue";
246
384
  }
@@ -271,6 +409,15 @@ function needsSpace(prev, current) {
271
409
  }
272
410
 
273
411
  if (current === "(") {
412
+ // Control-flow keywords read better with a space before the paren
413
+ // (`if (`, `while (`, `elseif (`), but calls like `function(` and
414
+ // `foo(` stay tight.
415
+ return keywordsSpacedBeforeParen.has(prev);
416
+ }
417
+
418
+ if (current === "[" && isValueEnd(prev)) {
419
+ // Indexing: `A[x]`, `t.k[x]`, `f()[x]` — no space. Table-constructor
420
+ // keys (`{ [k] = v }`) keep prev as `{`/`,`, so they are unaffected.
274
421
  return false;
275
422
  }
276
423
 
@@ -293,22 +440,56 @@ function needsSpace(prev, current) {
293
440
  return true;
294
441
  }
295
442
 
296
- function renderLine(tokens) {
443
+ function isGenericOpen(tokens, index) {
444
+ // tokens[index] is "<". Treat it as a generic-parameter list only in a
445
+ // declaration context: `function<T>`, `function foo<T>`, `type X<T>`.
446
+ const prev = tokens[index - 1];
447
+ if (prev === "function" || prev === "type") {
448
+ return true;
449
+ }
450
+
451
+ const prev2 = tokens[index - 2];
452
+ return (prev2 === "function" || prev2 === "type") &&
453
+ isIdentifier(prev) &&
454
+ !reservedKeywords.has(prev);
455
+ }
456
+
457
+ function joinTokens(tokens) {
297
458
  let text = "";
298
459
  let prev = null;
460
+ let genericDepth = 0;
299
461
 
300
- for (const token of tokens) {
301
- if (needsSpace(prev, token)) {
462
+ for (let idx = 0; idx < tokens.length; idx++) {
463
+ const token = tokens[idx];
464
+
465
+ const inGeneric = genericDepth > 0;
466
+ const opensGeneric = !inGeneric && token === "<" && isGenericOpen(tokens, idx);
467
+
468
+ if (prev !== null && !inGeneric && !opensGeneric && needsSpace(prev, token)) {
302
469
  text += " ";
303
470
  }
304
471
  text += token;
472
+
473
+ // Track generic-bracket nesting so the whole `<...>` stays tight.
474
+ if (opensGeneric || (inGeneric && token === "<")) {
475
+ genericDepth++;
476
+ } else if (inGeneric && token === ">") {
477
+ genericDepth--;
478
+ } else if (inGeneric && token === ">>") {
479
+ genericDepth = Math.max(0, genericDepth - 2);
480
+ }
481
+
305
482
  prev = token;
306
483
  }
307
484
 
308
- return text.trim();
485
+ return text;
309
486
  }
310
487
 
311
- function beautify(input) {
488
+ function renderLine(tokens) {
489
+ return joinTokens(tokens).trim();
490
+ }
491
+
492
+ function layout(input) {
312
493
  const tokens = tokenize(input);
313
494
  const lines = [];
314
495
  let current = [];
@@ -329,7 +510,7 @@ function beautify(input) {
329
510
 
330
511
  lines.push({
331
512
  depth: Math.max(0, depth),
332
- text: renderLine(current)
513
+ tokens: current
333
514
  });
334
515
  current = [];
335
516
  }
@@ -381,7 +562,7 @@ function beautify(input) {
381
562
  flushCurrent();
382
563
  lines.push({
383
564
  depth: Math.max(0, depth),
384
- text: token
565
+ tokens: [token]
385
566
  });
386
567
  continue;
387
568
  }
@@ -494,6 +675,23 @@ function beautify(input) {
494
675
  }
495
676
 
496
677
  if (token === ";") {
678
+ const table = currentTable();
679
+ const inTable = table &&
680
+ braceDepth === table.brace &&
681
+ parenDepth === table.paren &&
682
+ bracketDepth === table.bracket &&
683
+ blockBases.length === table.blockDepth;
684
+
685
+ if (inTable) {
686
+ // Inside a table constructor a `;` is just a field separator;
687
+ // normalize it to `,`.
688
+ current[current.length - 1] = ",";
689
+ if (table.multiline) {
690
+ flushCurrent();
691
+ }
692
+ continue;
693
+ }
694
+
497
695
  flushCurrent();
498
696
  continue;
499
697
  }
@@ -558,27 +756,78 @@ function beautify(input) {
558
756
 
559
757
  flushCurrent();
560
758
 
561
- return lines
562
- .map(line => `${indent.repeat(line.depth)}${line.text}`.trimEnd())
759
+ return lines;
760
+ }
761
+
762
+ function beautify(input) {
763
+ return layout(input)
764
+ .map(line => `${indent.repeat(line.depth)}${renderLine(line.tokens)}`.trimEnd())
563
765
  .join("\n");
564
766
  }
565
767
 
768
+ function isIdentChar(ch) {
769
+ return /[A-Za-z0-9_]/.test(ch);
770
+ }
771
+
772
+ function minifyNeedsSpace(prev, current) {
773
+ if (prev === null) {
774
+ return false;
775
+ }
776
+
777
+ const lastA = prev[prev.length - 1];
778
+ const firstB = current[0];
779
+
780
+ // Two word/number tokens would fuse into one (`local`+`x`, `1`+`e`).
781
+ if (isIdentChar(lastA) && isIdentChar(firstB)) {
782
+ return true;
783
+ }
784
+
785
+ // A numeric literal followed by `.`/`..` is ambiguous (`1`+`..` -> `1..`).
786
+ if (/^[0-9]/.test(prev) && firstB === ".") {
787
+ return true;
788
+ }
789
+
790
+ // Avoid two operator chars fusing into a longer token (`-`+`-` -> `--`
791
+ // comment, `.`+`.` -> `..`, `<`+`=` -> `<=`, `:`+`:` -> `::`, ...).
792
+ const pair = lastA + firstB;
793
+ if (pair === "--" || multiCharTokens.some(token => token.startsWith(pair))) {
794
+ return true;
795
+ }
796
+
797
+ return false;
798
+ }
799
+
566
800
  function minify(input) {
567
- const tokens = tokenize(input);
568
- const filtered = tokens.filter(t => !isComment(t));
801
+ const lines = layout(input).filter(
802
+ line => !(line.tokens.length === 1 && isComment(line.tokens[0]))
803
+ );
569
804
 
570
- let text = "";
805
+ let result = "";
571
806
  let prev = null;
572
807
 
573
- for (const token of filtered) {
574
- if (needsSpace(prev, token)) {
575
- text += " ";
808
+ for (const line of lines) {
809
+ const tokens = line.tokens;
810
+ for (let k = 0; k < tokens.length; k++) {
811
+ const token = tokens[k];
812
+
813
+ if (prev !== null) {
814
+ const callable = prev === ")" || prev === "]" ||
815
+ (isIdentifier(prev) && !reservedKeywords.has(prev));
816
+ if (k === 0 && token === "(" && callable) {
817
+ // A new statement starting with `(` after a prefix-expression
818
+ // would be parsed as a call continuation; separate with `;`.
819
+ result += ";";
820
+ } else if (minifyNeedsSpace(prev, token)) {
821
+ result += " ";
822
+ }
823
+ }
824
+
825
+ result += token;
826
+ prev = token;
576
827
  }
577
- text += token;
578
- prev = token;
579
828
  }
580
829
 
581
- return text;
830
+ return result;
582
831
  }
583
832
 
584
833
  module.exports = { beautify, minify };