nextlua 2.0.0 → 3.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/main.js +307 -25
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nextlua",
3
- "version": "2.0.0",
3
+ "version": "3.1.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,16 @@ function needsSpace(prev, current) {
271
409
  }
272
410
 
273
411
  if (current === "(") {
412
+ // Control-flow keywords and binary operators read better with a space
413
+ // before the paren (`if (`, `while (`, `a * (`, `x = (`), but calls
414
+ // like `function(` and `foo(` stay tight. A unary sign before `(` is
415
+ // handled by joinTokens, which suppresses the space (`-(x)`).
416
+ return keywordsSpacedBeforeParen.has(prev) || binaryOperators.has(prev);
417
+ }
418
+
419
+ if (current === "[" && isValueEnd(prev)) {
420
+ // Indexing: `A[x]`, `t.k[x]`, `f()[x]` — no space. Table-constructor
421
+ // keys (`{ [k] = v }`) keep prev as `{`/`,`, so they are unaffected.
274
422
  return false;
275
423
  }
276
424
 
@@ -282,7 +430,9 @@ function needsSpace(prev, current) {
282
430
  return false;
283
431
  }
284
432
 
285
- if (prev === "#" || current === "#") {
433
+ if (prev === "#") {
434
+ // The length operator binds tightly to its operand: `#h`, never `# h`.
435
+ // The space *before* `#` still follows the normal rules (`= #h`).
286
436
  return false;
287
437
  }
288
438
 
@@ -293,22 +443,75 @@ function needsSpace(prev, current) {
293
443
  return true;
294
444
  }
295
445
 
296
- function renderLine(tokens) {
446
+ function isGenericOpen(tokens, index) {
447
+ // tokens[index] is "<". Treat it as a generic-parameter list only in a
448
+ // declaration context: `function<T>`, `function foo<T>`, `type X<T>`.
449
+ const prev = tokens[index - 1];
450
+ if (prev === "function" || prev === "type") {
451
+ return true;
452
+ }
453
+
454
+ const prev2 = tokens[index - 2];
455
+ return (prev2 === "function" || prev2 === "type") &&
456
+ isIdentifier(prev) &&
457
+ !reservedKeywords.has(prev);
458
+ }
459
+
460
+ function isUnarySign(tokens, index) {
461
+ // tokens[index] is "-" or "+". It is unary (binding tightly to its operand
462
+ // with no following space: `-1`, `-x`, `-(y)`, `-#t`) when it does not
463
+ // follow a complete value. Otherwise it is the binary operator (`a - 1`).
464
+ const token = tokens[index];
465
+ if (token !== "-" && token !== "+") {
466
+ return false;
467
+ }
468
+ const before = tokens[index - 1];
469
+ return before === undefined || !isValueEnd(before);
470
+ }
471
+
472
+ function joinTokens(tokens) {
297
473
  let text = "";
298
474
  let prev = null;
475
+ let genericDepth = 0;
476
+
477
+ for (let idx = 0; idx < tokens.length; idx++) {
478
+ const token = tokens[idx];
299
479
 
300
- for (const token of tokens) {
301
- if (needsSpace(prev, token)) {
480
+ const inGeneric = genericDepth > 0;
481
+ const opensGeneric = !inGeneric && token === "<" && isGenericOpen(tokens, idx);
482
+
483
+ // A unary +/- binds to its operand, so suppress the space after it.
484
+ // (Skip the suppression when the next token is itself a +/- so we never
485
+ // fuse two signs into a `--` comment or a `+-` run.)
486
+ const prevIsUnarySign = prev !== null &&
487
+ token !== "-" && token !== "+" &&
488
+ isUnarySign(tokens, idx - 1);
489
+
490
+ if (prev !== null && !inGeneric && !opensGeneric && !prevIsUnarySign && needsSpace(prev, token)) {
302
491
  text += " ";
303
492
  }
304
493
  text += token;
494
+
495
+ // Track generic-bracket nesting so the whole `<...>` stays tight.
496
+ if (opensGeneric || (inGeneric && token === "<")) {
497
+ genericDepth++;
498
+ } else if (inGeneric && token === ">") {
499
+ genericDepth--;
500
+ } else if (inGeneric && token === ">>") {
501
+ genericDepth = Math.max(0, genericDepth - 2);
502
+ }
503
+
305
504
  prev = token;
306
505
  }
307
506
 
308
- return text.trim();
507
+ return text;
309
508
  }
310
509
 
311
- function beautify(input) {
510
+ function renderLine(tokens) {
511
+ return joinTokens(tokens).trim();
512
+ }
513
+
514
+ function layout(input) {
312
515
  const tokens = tokenize(input);
313
516
  const lines = [];
314
517
  let current = [];
@@ -329,7 +532,7 @@ function beautify(input) {
329
532
 
330
533
  lines.push({
331
534
  depth: Math.max(0, depth),
332
- text: renderLine(current)
535
+ tokens: current
333
536
  });
334
537
  current = [];
335
538
  }
@@ -381,7 +584,7 @@ function beautify(input) {
381
584
  flushCurrent();
382
585
  lines.push({
383
586
  depth: Math.max(0, depth),
384
- text: token
587
+ tokens: [token]
385
588
  });
386
589
  continue;
387
590
  }
@@ -390,6 +593,17 @@ function beautify(input) {
390
593
  flushCurrent();
391
594
  }
392
595
 
596
+ if (token === "else" && inlineIfStack.length && inlineIfStack[inlineIfStack.length - 1] === "else") {
597
+ // The `else` of an inline `if ... then ... else ...` expression.
598
+ // It can appear nested inside parens/brackets (e.g. `x = (if c then
599
+ // a else b)`), so it must be matched regardless of bracket depth —
600
+ // otherwise inlineIfStack never empties and statement-splitting
601
+ // stays disabled for the rest of the block.
602
+ inlineIfStack.pop();
603
+ current.push(token);
604
+ continue;
605
+ }
606
+
393
607
  if (blockMiddle.has(token) && atStatementLevel()) {
394
608
  if (token === "else" && inlineIfStack.length) {
395
609
  inlineIfStack.pop();
@@ -494,6 +708,23 @@ function beautify(input) {
494
708
  }
495
709
 
496
710
  if (token === ";") {
711
+ const table = currentTable();
712
+ const inTable = table &&
713
+ braceDepth === table.brace &&
714
+ parenDepth === table.paren &&
715
+ bracketDepth === table.bracket &&
716
+ blockBases.length === table.blockDepth;
717
+
718
+ if (inTable) {
719
+ // Inside a table constructor a `;` is just a field separator;
720
+ // normalize it to `,`.
721
+ current[current.length - 1] = ",";
722
+ if (table.multiline) {
723
+ flushCurrent();
724
+ }
725
+ continue;
726
+ }
727
+
497
728
  flushCurrent();
498
729
  continue;
499
730
  }
@@ -558,27 +789,78 @@ function beautify(input) {
558
789
 
559
790
  flushCurrent();
560
791
 
561
- return lines
562
- .map(line => `${indent.repeat(line.depth)}${line.text}`.trimEnd())
792
+ return lines;
793
+ }
794
+
795
+ function beautify(input) {
796
+ return layout(input)
797
+ .map(line => `${indent.repeat(line.depth)}${renderLine(line.tokens)}`.trimEnd())
563
798
  .join("\n");
564
799
  }
565
800
 
801
+ function isIdentChar(ch) {
802
+ return /[A-Za-z0-9_]/.test(ch);
803
+ }
804
+
805
+ function minifyNeedsSpace(prev, current) {
806
+ if (prev === null) {
807
+ return false;
808
+ }
809
+
810
+ const lastA = prev[prev.length - 1];
811
+ const firstB = current[0];
812
+
813
+ // Two word/number tokens would fuse into one (`local`+`x`, `1`+`e`).
814
+ if (isIdentChar(lastA) && isIdentChar(firstB)) {
815
+ return true;
816
+ }
817
+
818
+ // A numeric literal followed by `.`/`..` is ambiguous (`1`+`..` -> `1..`).
819
+ if (/^[0-9]/.test(prev) && firstB === ".") {
820
+ return true;
821
+ }
822
+
823
+ // Avoid two operator chars fusing into a longer token (`-`+`-` -> `--`
824
+ // comment, `.`+`.` -> `..`, `<`+`=` -> `<=`, `:`+`:` -> `::`, ...).
825
+ const pair = lastA + firstB;
826
+ if (pair === "--" || multiCharTokens.some(token => token.startsWith(pair))) {
827
+ return true;
828
+ }
829
+
830
+ return false;
831
+ }
832
+
566
833
  function minify(input) {
567
- const tokens = tokenize(input);
568
- const filtered = tokens.filter(t => !isComment(t));
834
+ const lines = layout(input).filter(
835
+ line => !(line.tokens.length === 1 && isComment(line.tokens[0]))
836
+ );
569
837
 
570
- let text = "";
838
+ let result = "";
571
839
  let prev = null;
572
840
 
573
- for (const token of filtered) {
574
- if (needsSpace(prev, token)) {
575
- text += " ";
841
+ for (const line of lines) {
842
+ const tokens = line.tokens;
843
+ for (let k = 0; k < tokens.length; k++) {
844
+ const token = tokens[k];
845
+
846
+ if (prev !== null) {
847
+ const callable = prev === ")" || prev === "]" ||
848
+ (isIdentifier(prev) && !reservedKeywords.has(prev));
849
+ if (k === 0 && token === "(" && callable) {
850
+ // A new statement starting with `(` after a prefix-expression
851
+ // would be parsed as a call continuation; separate with `;`.
852
+ result += ";";
853
+ } else if (minifyNeedsSpace(prev, token)) {
854
+ result += " ";
855
+ }
856
+ }
857
+
858
+ result += token;
859
+ prev = token;
576
860
  }
577
- text += token;
578
- prev = token;
579
861
  }
580
862
 
581
- return text;
863
+ return result;
582
864
  }
583
865
 
584
866
  module.exports = { beautify, minify };