rapydscript-ng 0.7.24 → 0.8.1

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 (132) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +80 -26
  3. package/bin/build.ts +117 -0
  4. package/bin/package.json +1 -0
  5. package/bin/rapydscript +65 -62
  6. package/build_wheels.py +133 -0
  7. package/editor-plugins/README.md +80 -0
  8. package/editor-plugins/nvim.lua +321 -0
  9. package/local-agent.md +28 -0
  10. package/package.json +4 -5
  11. package/publish.py +27 -17
  12. package/release/baselib-plain-pretty.js +448 -326
  13. package/release/baselib-plain-ugly.js +3 -3
  14. package/release/compiler.js +9027 -8139
  15. package/release/signatures.json +27 -27
  16. package/release/stdlib_modules.json +1 -0
  17. package/src/ast.pyj +428 -280
  18. package/src/baselib-builtins.pyj +49 -28
  19. package/src/baselib-containers.pyj +241 -226
  20. package/src/baselib-errors.pyj +8 -1
  21. package/src/baselib-internal.pyj +38 -23
  22. package/src/baselib-itertools.pyj +13 -7
  23. package/src/baselib-str.pyj +59 -84
  24. package/src/compiler.pyj +4 -4
  25. package/src/errors.pyj +3 -4
  26. package/src/lib/aes.pyj +46 -32
  27. package/src/lib/elementmaker.pyj +11 -9
  28. package/src/lib/encodings.pyj +15 -5
  29. package/src/lib/gettext.pyj +9 -4
  30. package/src/lib/math.pyj +106 -45
  31. package/src/lib/operator.pyj +10 -10
  32. package/src/lib/pythonize.pyj +2 -1
  33. package/src/lib/random.pyj +28 -21
  34. package/src/lib/re.pyj +492 -19
  35. package/src/lib/traceback.pyj +171 -29
  36. package/src/lib/uuid.pyj +2 -2
  37. package/src/output/classes.pyj +28 -27
  38. package/src/output/codegen.pyj +87 -83
  39. package/src/output/comments.pyj +8 -8
  40. package/src/output/exceptions.pyj +20 -21
  41. package/src/output/functions.pyj +66 -64
  42. package/src/output/literals.pyj +24 -23
  43. package/src/output/loops.pyj +84 -142
  44. package/src/output/modules.pyj +55 -68
  45. package/src/output/operators.pyj +40 -29
  46. package/src/output/statements.pyj +21 -15
  47. package/src/output/stream.pyj +82 -56
  48. package/src/output/utils.pyj +12 -8
  49. package/src/parse.pyj +971 -620
  50. package/src/string_interpolation.pyj +5 -3
  51. package/src/tokenizer.pyj +188 -148
  52. package/src/utils.pyj +32 -15
  53. package/test/_treeshake_mod.pyj +30 -0
  54. package/test/_treeshake_side_effect.pyj +9 -0
  55. package/test/ast_serialization.pyj +392 -0
  56. package/test/async.pyj +95 -0
  57. package/test/baselib.pyj +1 -2
  58. package/test/embedded_compiler.pyj +57 -0
  59. package/test/fmt.pyj +291 -0
  60. package/test/generic.pyj +16 -3
  61. package/test/imports.pyj +8 -6
  62. package/test/internationalization.pyj +38 -37
  63. package/test/lint.pyj +120 -117
  64. package/test/lsp.pyj +257 -0
  65. package/test/repl.pyj +70 -65
  66. package/test/sourcemaps.pyj +315 -0
  67. package/test/starargs.pyj +46 -39
  68. package/test/str.pyj +8 -0
  69. package/test/traceback.pyj +263 -0
  70. package/test/treeshaking.pyj +181 -0
  71. package/test/web_repl_export.pyj +88 -0
  72. package/tools/ast_serialize.mjs +557 -0
  73. package/tools/cli.mjs +531 -0
  74. package/tools/compile.mjs +205 -0
  75. package/tools/compiler.mjs +159 -0
  76. package/tools/{completer.js → completer.mjs} +6 -6
  77. package/tools/{embedded_compiler.js → embedded_compiler.mjs} +17 -13
  78. package/tools/export.js +109 -56
  79. package/tools/fmt.mjs +975 -0
  80. package/tools/{gettext.js → gettext.mjs} +46 -53
  81. package/tools/ini.mjs +175 -0
  82. package/tools/{lint.js → lint.mjs} +78 -55
  83. package/tools/lsp.mjs +964 -0
  84. package/tools/lsp_protocol.mjs +259 -0
  85. package/tools/lsp_symbols.mjs +418 -0
  86. package/tools/{msgfmt.js → msgfmt.mjs} +18 -18
  87. package/tools/{repl.js → repl.mjs} +56 -42
  88. package/tools/{self.js → self.mjs} +71 -46
  89. package/tools/sourcemap.mjs +123 -0
  90. package/tools/test.mjs +252 -0
  91. package/tools/treeshake.mjs +400 -0
  92. package/tools/{utils.js → utils.mjs} +26 -23
  93. package/tools/{web_repl.js → web_repl.mjs} +15 -13
  94. package/tools/web_repl_export.mjs +97 -0
  95. package/tree-sitter/README.md +101 -0
  96. package/tree-sitter/grammar.js +992 -0
  97. package/tree-sitter/package.json +43 -0
  98. package/tree-sitter/queries/highlights.scm +232 -0
  99. package/tree-sitter/queries/indents.scm +64 -0
  100. package/tree-sitter/queries/injections.scm +14 -0
  101. package/tree-sitter/queries/locals.scm +108 -0
  102. package/tree-sitter/src/grammar.json +4976 -0
  103. package/tree-sitter/src/node-types.json +2901 -0
  104. package/tree-sitter/src/parser.c +196465 -0
  105. package/tree-sitter/src/scanner.c +294 -0
  106. package/tree-sitter/src/tree_sitter/alloc.h +54 -0
  107. package/tree-sitter/src/tree_sitter/array.h +330 -0
  108. package/tree-sitter/src/tree_sitter/parser.h +286 -0
  109. package/tree-sitter/test/corpus/chaining.txt +99 -0
  110. package/tree-sitter/test/corpus/classes.txt +147 -0
  111. package/tree-sitter/test/corpus/comprehensions.txt +155 -0
  112. package/tree-sitter/test/corpus/containers.txt +242 -0
  113. package/tree-sitter/test/corpus/control_flow.txt +361 -0
  114. package/tree-sitter/test/corpus/decorators.txt +64 -0
  115. package/tree-sitter/test/corpus/existential.txt +102 -0
  116. package/tree-sitter/test/corpus/functions.txt +293 -0
  117. package/tree-sitter/test/corpus/imports.txt +117 -0
  118. package/tree-sitter/test/corpus/literals.txt +135 -0
  119. package/tree-sitter/test/corpus/operators.txt +296 -0
  120. package/tree-sitter/test/corpus/statements.txt +189 -0
  121. package/tree-sitter/test/corpus/strings.txt +90 -0
  122. package/tree-sitter/test/corpus/subscripts.txt +227 -0
  123. package/tree-sitter/tree-sitter.json +36 -0
  124. package/web-repl/env.js +35 -23
  125. package/web-repl/main.js +8 -8
  126. package/bin/export +0 -75
  127. package/bin/web-repl-export +0 -102
  128. package/tools/cli.js +0 -523
  129. package/tools/compile.js +0 -184
  130. package/tools/compiler.js +0 -84
  131. package/tools/ini.js +0 -65
  132. package/tools/test.js +0 -110
package/tools/fmt.mjs ADDED
@@ -0,0 +1,975 @@
1
+ /* vim:fileencoding=utf-8
2
+ *
3
+ * fmt.mjs -- A PEP8 style code formatter for RapydScript source code.
4
+ *
5
+ * Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
6
+ *
7
+ * Distributed under terms of the BSD license.
8
+ *
9
+ * The formatter deliberately does not reuse the compiler's tokenizer because
10
+ * that tokenizer is lossy for a formatter: it decodes string escapes, converts
11
+ * numbers to JavaScript values, turns regexps into RegExp objects, rewrites
12
+ * f-strings by mutating the source buffer and maps operators (and -> &&, is ->
13
+ * === etc.). Instead we use a small, purpose built, loss-less lexer that
14
+ * preserves the exact source lexeme of every token.
15
+ *
16
+ * RapydScript is close to, but not identical to, Python. In particular it
17
+ * supports multi-line anonymous functions, leading-dot call chaining, verbatim
18
+ * JavaScript string/regex literals and newline sensitive blocks. Reformatting
19
+ * these constructs incorrectly would change program semantics, so the formatter
20
+ * is conservative: "complex" logical lines (those containing anonymous
21
+ * functions, block colons that span lines, semicolon inlined blocks, leading
22
+ * dot chains, backslash continuations or multi-line string/regex literals) have
23
+ * their spacing/indentation/quotes normalized but their physical line structure
24
+ * preserved -- they are never reflowed onto a single line nor wrapped.
25
+ */
26
+ "use strict";
27
+
28
+ import fs from 'fs';
29
+ import path from 'path';
30
+ import { fileURLToPath } from 'url';
31
+ import { read_pyproject_config } from './ini.mjs';
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Lexer
35
+ // ---------------------------------------------------------------------------
36
+
37
+ var ATOMS = new Set(['True', 'False', 'None']);
38
+ // Keywords that stay keywords (word operators like and/or/not/in/is/new/del are
39
+ // classified as operators, mirroring the compiler's tokenizer).
40
+ var KEYWORDS = new Set([
41
+ 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'do',
42
+ 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import',
43
+ 'nonlocal', 'pass', 'raise', 'return', 'yield', 'try', 'while', 'with',
44
+ ]);
45
+ var WORD_OPS = new Set(['and', 'or', 'not', 'in', 'is', 'new', 'del', 'void', 'typeof', 'instanceof']);
46
+ // After these tokens a `/` begins a regular expression rather than division.
47
+ var KW_BEFORE_EXPR = new Set(['return', 'yield', 'raise', 'elif', 'else', 'if', 'await', 'in', 'assert', 'while']);
48
+ var PUNC_BEFORE_EXPR = new Set(['[', '{', '(', ',', '.', ';', ':']);
49
+ var PUNC_CHARS = new Set(['(', ')', '[', ']', '{', '}', ',', ';', ':', '?']);
50
+ var STRING_MOD = /^[vrufVRUF]+$/;
51
+
52
+ // Symbolic operators, ordered longest-first for greedy matching.
53
+ var OPERATORS = [
54
+ '>>>=', '>>>', '>>=', '<<=', '//=', '**=', '>>', '<<', '//', '**', '<=',
55
+ '>=', '==', '!=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '->',
56
+ '~', '+', '-', '*', '/', '%', '<', '>', '=', '&', '|', '^', '@', '!',
57
+ ];
58
+ var OPERATOR_CHARS = new Set('+-*&%=<>!?|~^@/'.split(''));
59
+
60
+ function is_ws(c) { return c === ' ' || c === '\t' || c === '\f' || c === '\v' || c === ' '; }
61
+ function is_digit(c) { return c >= '0' && c <= '9'; }
62
+ function is_ident_start(c) {
63
+ if (c === '_' || c === '$') return true;
64
+ var k = c.charCodeAt(0);
65
+ return (k >= 65 && k <= 90) || (k >= 97 && k <= 122) || k > 127;
66
+ }
67
+ function is_ident_char(c) { return is_ident_start(c) || is_digit(c); }
68
+
69
+ function FormatError(message, line) {
70
+ var e = new Error(message + (line ? ' (line ' + line + ')' : ''));
71
+ e.is_format_error = true;
72
+ return e;
73
+ }
74
+
75
+ function lex(text) {
76
+ var n = text.length;
77
+ var pos = 0, line = 1;
78
+ var toks = [];
79
+ var prev_sig = null; // previous non-comment token, for regex/kind decisions
80
+
81
+ function regex_allowed() {
82
+ if (!prev_sig) return true;
83
+ if (prev_sig.type === 'op') return true;
84
+ if (prev_sig.type === 'keyword' && KW_BEFORE_EXPR.has(prev_sig.value)) return true;
85
+ if (prev_sig.type === 'punc' && PUNC_BEFORE_EXPR.has(prev_sig.value)) return true;
86
+ return false;
87
+ }
88
+
89
+ function consume_ws() {
90
+ var nlb = 0, had_ws = false, cont = false;
91
+ while (pos < n) {
92
+ var c = text[pos];
93
+ if (c === '\\' && text[pos + 1] === '\n') { pos += 2; line++; nlb++; cont = true; continue; }
94
+ if (c === '\n') { nlb++; line++; pos++; had_ws = true; continue; }
95
+ if (is_ws(c)) { had_ws = true; pos++; continue; }
96
+ break;
97
+ }
98
+ // Leading whitespace of the current physical line.
99
+ var ls = pos;
100
+ while (ls > 0 && text[ls - 1] !== '\n') ls--;
101
+ var ie = ls;
102
+ while (ie < n && is_ws(text[ie])) ie++;
103
+ return { nlb: nlb, sp: (had_ws && nlb === 0), cont: cont, indent: text.slice(ls, ie) };
104
+ }
105
+
106
+ function read_string_body(qpos) {
107
+ // qpos points at the opening quote (prefix already consumed). Advances pos
108
+ // past the closing quote and returns nothing; caller slices the lexeme.
109
+ var q = text[qpos];
110
+ if (text[qpos + 1] === q && text[qpos + 2] === q) {
111
+ pos = qpos + 3;
112
+ while (pos < n) {
113
+ if (text[pos] === '\\') { pos += 2; continue; }
114
+ if (text[pos] === q && text[pos + 1] === q && text[pos + 2] === q) { pos += 3; return; }
115
+ if (text[pos] === '\n') line++;
116
+ pos++;
117
+ }
118
+ throw FormatError('Unterminated triple-quoted string', line);
119
+ }
120
+ pos = qpos + 1;
121
+ while (pos < n) {
122
+ var c = text[pos];
123
+ if (c === '\\') { pos += 2; continue; }
124
+ if (c === q) { pos++; return; }
125
+ if (c === '\n') throw FormatError('End of line while scanning string literal', line);
126
+ pos++;
127
+ }
128
+ throw FormatError('Unterminated string', line);
129
+ }
130
+
131
+ function read_regexp() {
132
+ var start = pos;
133
+ pos++; // first '/'
134
+ if (text[pos] === '/') {
135
+ pos++;
136
+ if (text[pos] === '/') {
137
+ pos++; // verbose ///.../// regex
138
+ while (pos < n) {
139
+ if (text[pos] === '\\') { pos += 2; continue; }
140
+ if (text[pos] === '/' && text[pos + 1] === '/' && text[pos + 2] === '/') { pos += 3; break; }
141
+ if (text[pos] === '\n') line++;
142
+ pos++;
143
+ }
144
+ }
145
+ // else: empty regexp //, nothing more to read before modifiers
146
+ } else {
147
+ var in_class = false;
148
+ while (pos < n) {
149
+ var c = text[pos];
150
+ if (c === '\\') { pos += 2; continue; }
151
+ if (c === '[') { in_class = true; pos++; continue; }
152
+ if (c === ']' && in_class) { in_class = false; pos++; continue; }
153
+ if (c === '/' && !in_class) { pos++; break; }
154
+ if (c === '\n') { throw FormatError('Unterminated regular expression', line); }
155
+ pos++;
156
+ }
157
+ }
158
+ while (pos < n && /[a-zA-Z]/.test(text[pos])) pos++;
159
+ return text.slice(start, pos);
160
+ }
161
+
162
+ function read_number(from_dot) {
163
+ var start = pos;
164
+ if (!from_dot && text[pos] === '0' && (text[pos + 1] === 'x' || text[pos + 1] === 'X')) {
165
+ pos += 2;
166
+ while (pos < n && /[0-9a-fA-F]/.test(text[pos])) pos++;
167
+ } else if (!from_dot && text[pos] === '0' && (text[pos + 1] === 'b' || text[pos + 1] === 'B')) {
168
+ pos += 2;
169
+ while (pos < n && (text[pos] === '0' || text[pos] === '1')) pos++;
170
+ } else {
171
+ if (from_dot) { pos++; while (pos < n && is_digit(text[pos])) pos++; }
172
+ else {
173
+ while (pos < n && is_digit(text[pos])) pos++;
174
+ if (text[pos] === '.') { pos++; while (pos < n && is_digit(text[pos])) pos++; }
175
+ }
176
+ if (text[pos] === 'e' || text[pos] === 'E') {
177
+ pos++;
178
+ if (text[pos] === '+' || text[pos] === '-') pos++;
179
+ while (pos < n && is_digit(text[pos])) pos++;
180
+ }
181
+ }
182
+ return text.slice(start, pos);
183
+ }
184
+
185
+ function match_operator() {
186
+ for (var i = 0; i < OPERATORS.length; i++) {
187
+ if (text.startsWith(OPERATORS[i], pos)) return OPERATORS[i];
188
+ }
189
+ return text[pos];
190
+ }
191
+
192
+ function push(type, value, ws) {
193
+ var t = {
194
+ type: type, value: value, nlb: ws.nlb, sp: ws.sp, cont: ws.cont,
195
+ line_indent: ws.indent, line: line,
196
+ };
197
+ toks.push(t);
198
+ if (type !== 'comment' && type !== 'shebang') prev_sig = t;
199
+ return t;
200
+ }
201
+
202
+ while (pos < n) {
203
+ var ws = consume_ws();
204
+ if (pos >= n) break;
205
+ var c = text[pos];
206
+
207
+ if (c === '#') {
208
+ var c_start = pos;
209
+ var eol = text.indexOf('\n', pos);
210
+ if (eol === -1) eol = n;
211
+ var raw = text.slice(pos, eol);
212
+ pos = eol;
213
+ if (c_start === 0 && raw[1] === '!') push('shebang', raw, ws);
214
+ else push('comment', raw, ws);
215
+ continue;
216
+ }
217
+ if (c === '"' || c === "'") {
218
+ var s = pos;
219
+ read_string_body(pos);
220
+ push('string', text.slice(s, pos), ws);
221
+ continue;
222
+ }
223
+ if (is_ident_start(c)) {
224
+ var ws_start = pos;
225
+ while (pos < n && is_ident_char(text[pos])) pos++;
226
+ var word = text.slice(ws_start, pos);
227
+ if (STRING_MOD.test(word) && (text[pos] === '"' || text[pos] === "'")) {
228
+ var qpos = pos;
229
+ read_string_body(pos);
230
+ push('string', word + text.slice(qpos, pos), ws);
231
+ continue;
232
+ }
233
+ if (ATOMS.has(word)) push('atom', word, ws);
234
+ else if (WORD_OPS.has(word)) push('op', word, ws);
235
+ else if (KEYWORDS.has(word)) push('keyword', word, ws);
236
+ else push('name', word, ws);
237
+ continue;
238
+ }
239
+ if (is_digit(c)) { push('number', read_number(false), ws); continue; }
240
+ if (c === '.') {
241
+ if (is_digit(text[pos + 1])) { push('number', read_number(true), ws); continue; }
242
+ pos++; push('punc', '.', ws); continue;
243
+ }
244
+ if (c === '/') {
245
+ if (regex_allowed()) { push('regexp', read_regexp(), ws); continue; }
246
+ var op = match_operator(); pos += op.length; push('op', op, ws); continue;
247
+ }
248
+ if (PUNC_CHARS.has(c)) { pos++; push('punc', c, ws); continue; }
249
+ if (OPERATOR_CHARS.has(c)) { var op2 = match_operator(); pos += op2.length; push('op', op2, ws); continue; }
250
+ throw FormatError('Unexpected character «' + c + '»', line);
251
+ }
252
+
253
+ push('eof', '', { nlb: 0, sp: false, cont: false, indent: '' });
254
+ return toks;
255
+ }
256
+
257
+ // ---------------------------------------------------------------------------
258
+ // Token rendering (spacing engine)
259
+ // ---------------------------------------------------------------------------
260
+
261
+ function is_operand(p) {
262
+ if (!p) return false;
263
+ if (p.type === 'name' || p.type === 'number' || p.type === 'string' || p.type === 'atom' || p.type === 'regexp') return true;
264
+ return p.value === ')' || p.value === ']' || p.value === '}';
265
+ }
266
+
267
+ function is_call_paren(p) {
268
+ if (!p) return false;
269
+ if (p.type === 'name' || p.type === 'string' || p.type === 'atom') return true;
270
+ return p.value === ')' || p.value === ']' || p.value === '}';
271
+ }
272
+
273
+ function is_index_bracket(p) {
274
+ if (!p) return false;
275
+ if (p.type === 'name' || p.type === 'string' || p.type === 'number' || p.type === 'atom') return true;
276
+ return p.value === ')' || p.value === ']' || p.value === '}';
277
+ }
278
+
279
+ function is_unary_here(p, t) {
280
+ if (t.type === 'op') {
281
+ var v = t.value;
282
+ if (v === '+' || v === '-' || v === '~' || v === '*' || v === '**') return !is_operand(p);
283
+ if (v === '@' && p === null) return true;
284
+ }
285
+ return false;
286
+ }
287
+
288
+ function separator(p, t, stack, prev_was_unary, sig_count, first_at) {
289
+ var pv = p.value, tv = t.value, pty = p.type, tty = t.type;
290
+ var top = stack.length ? stack[stack.length - 1] : null;
291
+ if (pty === 'punc' && (pv === '(' || pv === '[' || pv === '{')) return false;
292
+ if (tty === 'punc' && (tv === ')' || tv === ']' || tv === '}')) return false;
293
+ if (tv === ',' || tv === ';') return false;
294
+ if (tv === '.' || pv === '.') return false;
295
+ if (tv === '?' || pv === '?') return !!t.sp; // existential: preserve source spacing
296
+ if (tv === ':') return false;
297
+ if (pv === ':') { if (top && top.kind === 'index') return false; return true; }
298
+ if (pv === '@' && first_at && sig_count === 1) return false; // decorator
299
+ if (tv === '(' && is_call_paren(p)) return false;
300
+ if (tv === '[' && is_index_bracket(p)) return false;
301
+ if ((tv === '=' || pv === '=') && top && top.kind === 'call' && !top.in_def_body) return false; // kwarg / default
302
+ if (prev_was_unary) return false;
303
+ return true;
304
+ }
305
+
306
+ function format_comment(raw) {
307
+ var m = raw.match(/^#+/)[0];
308
+ var rest = raw.slice(m.length).replace(/\s+$/, '');
309
+ if (rest === '') return m;
310
+ if (rest[0] !== ' ' && rest[0] !== '!') rest = ' ' + rest;
311
+ return m + rest;
312
+ }
313
+
314
+ function normalize_quotes(lex_val, preferred) {
315
+ var i = 0;
316
+ while (i < lex_val.length && /[vrufVRUF]/.test(lex_val[i])) i++;
317
+ var prefix = lex_val.slice(0, i);
318
+ var q = lex_val[i];
319
+ if (q !== "'" && q !== '"') return lex_val;
320
+ var lower = prefix.toLowerCase();
321
+ if (lower.indexOf('v') >= 0 || lower.indexOf('r') >= 0 || lower.indexOf('f') >= 0) return lex_val;
322
+ if (lex_val[i + 1] === q && lex_val[i + 2] === q) return lex_val; // triple-quoted: leave as-is
323
+ var target = preferred;
324
+ if (q === target) return lex_val;
325
+ var body = lex_val.slice(i + 1, lex_val.length - 1);
326
+ var nb = '', j = 0;
327
+ while (j < body.length) {
328
+ var c = body[j];
329
+ if (c === '\\' && j + 1 < body.length) {
330
+ var nx = body[j + 1];
331
+ if (nx === q) { nb += q; j += 2; continue; } // escaped old quote -> unescape
332
+ nb += '\\' + nx; j += 2; continue;
333
+ }
334
+ if (c === target) { nb += '\\' + target; j++; continue; } // must escape target
335
+ nb += c; j++;
336
+ }
337
+ var count = function (s) { var k = 0; for (var x = 0; x < s.length; x++) if (s[x] === '\\') k++; return k; };
338
+ if (count(nb) > count(body)) return lex_val; // don't increase escapes
339
+ return prefix + target + nb + target;
340
+ }
341
+
342
+ function token_text(t, opts) {
343
+ if (t.type === 'string') return normalize_quotes(t.value, opts.preferred);
344
+ return t.value;
345
+ }
346
+
347
+ // Render a list of tokens to a string. When honor_breaks is true, source line
348
+ // breaks (nlb>0) are preserved as newlines + re-based indentation; otherwise
349
+ // everything is emitted on one line (reflow).
350
+ function render(toks, honor_breaks, src_base, new_base, opts, initial_stack) {
351
+ var out = '';
352
+ var stack = initial_stack ? initial_stack.slice() : [];
353
+ var prev_sig = null, prev_was_unary = false, sig_count = 0, first_at = false;
354
+ // after_def: true after seeing 'def' (and optionally a function name), so the
355
+ // next '(' can be identified as the def's parameter list.
356
+ // prev_closed_def_params: true when we just popped a def parameter list ')'.
357
+ // When the very next ':' is seen we mark the enclosing stack frame in_def_body
358
+ // so that '=' tokens inside the body are not suppressed as kwargs.
359
+ var after_def = false, prev_closed_def_params = false;
360
+ for (var k = 0; k < toks.length; k++) {
361
+ var t = toks[k];
362
+ var did_break = false;
363
+ if (k > 0 && honor_breaks && t.nlb > 0) {
364
+ out += (t.cont ? ' \\\n' : '\n');
365
+ var li = t.line_indent || '';
366
+ // Rebase indentation that shares the statement's base prefix; preserve
367
+ // dedented continuation lines verbatim (e.g. a leading-dot line that
368
+ // binds to an outer block, or a `.while` clause of a do/while loop).
369
+ if (li.startsWith(src_base)) out += new_base + li.slice(src_base.length);
370
+ else out += li;
371
+ did_break = true;
372
+ }
373
+ if (t.type === 'comment' || t.type === 'shebang') {
374
+ var ctext = (t.type === 'shebang') ? t.value : format_comment(t.value);
375
+ if (k === 0 || did_break) out += ctext;
376
+ else out += ' ' + ctext;
377
+ continue;
378
+ }
379
+ if (k > 0 && !did_break && prev_sig !== null) {
380
+ if (separator(prev_sig, t, stack, prev_was_unary, sig_count, first_at)) out += ' ';
381
+ }
382
+ out += token_text(t, opts);
383
+ prev_was_unary = is_unary_here(prev_sig, t);
384
+ if (t.type === 'punc') {
385
+ if (t.value === '(') {
386
+ stack.push({ kind: is_call_paren(prev_sig) ? 'call' : 'group', is_def_params: after_def });
387
+ after_def = false; prev_closed_def_params = false;
388
+ } else if (t.value === '[') {
389
+ stack.push({ kind: is_index_bracket(prev_sig) ? 'index' : 'list' });
390
+ after_def = false; prev_closed_def_params = false;
391
+ } else if (t.value === '{') {
392
+ stack.push({ kind: 'dict' });
393
+ after_def = false; prev_closed_def_params = false;
394
+ } else if (t.value === ')' || t.value === ']' || t.value === '}') {
395
+ var popped = stack.length ? stack.pop() : null;
396
+ prev_closed_def_params = !!(popped && popped.is_def_params);
397
+ after_def = false;
398
+ } else if (t.value === ':') {
399
+ if (prev_closed_def_params && stack.length > 0) {
400
+ // Only mark in_def_body for multi-line def bodies. For inline
401
+ // defs (def():stmt; inside a call), the body is on the same
402
+ // line and subsequent same-depth tokens are still kwargs.
403
+ var next_is_newline = false;
404
+ for (var nk = k + 1; nk < toks.length; nk++) {
405
+ if (toks[nk].type === 'eof') break;
406
+ if (toks[nk].type !== 'comment' && toks[nk].type !== 'shebang') {
407
+ next_is_newline = toks[nk].nlb > 0; break;
408
+ }
409
+ }
410
+ if (next_is_newline) stack[stack.length - 1].in_def_body = true;
411
+ }
412
+ prev_closed_def_params = false; after_def = false;
413
+ } else {
414
+ after_def = false; prev_closed_def_params = false;
415
+ }
416
+ } else if (t.type === 'keyword' && t.value === 'def') {
417
+ after_def = true; prev_closed_def_params = false;
418
+ } else if (after_def && t.type === 'name') {
419
+ prev_closed_def_params = false; // named def: keep after_def for next '('
420
+ } else {
421
+ after_def = false; prev_closed_def_params = false;
422
+ }
423
+ if (sig_count === 0 && t.value === '@' && t.type === 'op') first_at = true;
424
+ prev_sig = t; sig_count++;
425
+ }
426
+ return out;
427
+ }
428
+
429
+ // ---------------------------------------------------------------------------
430
+ // Line wrapping (conservative)
431
+ // ---------------------------------------------------------------------------
432
+
433
+ function wrap_line(code, level, opts) {
434
+ // Locate the top-level (depth 0 -> 1) bracket group with commas and the
435
+ // largest span, then explode it across multiple lines with hanging indent.
436
+ var depth = 0, open_stack = [], groups = [];
437
+ for (var i = 0; i < code.length; i++) {
438
+ var v = code[i].value, ty = code[i].type;
439
+ if (ty === 'punc' && (v === '(' || v === '[' || v === '{')) { if (depth === 0) open_stack.push({ open: i, ch: v }); depth++; }
440
+ else if (ty === 'punc' && (v === ')' || v === ']' || v === '}')) { depth--; if (depth === 0 && open_stack.length) { var g = open_stack.pop(); g.close = i; groups.push(g); } }
441
+ }
442
+ var best = null;
443
+ for (var gi = 0; gi < groups.length; gi++) {
444
+ var grp = groups[gi];
445
+ if (grp.close === undefined) continue;
446
+ var d = 0, has_comma = false, is_comprehension = false;
447
+ for (var x = grp.open + 1; x < grp.close; x++) {
448
+ var vv = code[x].value, tt = code[x].type;
449
+ if (tt === 'punc' && (vv === '(' || vv === '[' || vv === '{')) d++;
450
+ else if (tt === 'punc' && (vv === ')' || vv === ']' || vv === '}')) d--;
451
+ else if (d === 0 && tt === 'punc' && vv === ',') has_comma = true;
452
+ else if (d === 0 && tt === 'keyword' && vv === 'for') is_comprehension = true;
453
+ }
454
+ // In a comprehension/generator the top-level commas are tuple unpacking,
455
+ // not element separators, so it is not safe to explode on them.
456
+ if (!has_comma || is_comprehension) continue;
457
+ var span = grp.close - grp.open;
458
+ if (!best || span > best.span) best = { open: grp.open, close: grp.close, ch: grp.ch, span: span };
459
+ }
460
+ if (!best) return null;
461
+
462
+ var base_indent = ' '.repeat(4 * level);
463
+ var child_indent = ' '.repeat(4 * (level + 1));
464
+ var open_ch = best.ch;
465
+ var seed_kind = (open_ch === '(') ? (is_call_paren(best.open > 0 ? code[best.open - 1] : null) ? 'call' : 'group')
466
+ : (open_ch === '[') ? (is_index_bracket(best.open > 0 ? code[best.open - 1] : null) ? 'index' : 'list')
467
+ : 'dict';
468
+
469
+ var head = code.slice(0, best.open + 1);
470
+ var head_str = base_indent + render(head, false, '', base_indent, opts);
471
+
472
+ var inner = code.slice(best.open + 1, best.close);
473
+ var elems = [], cur = [], dd = 0;
474
+ for (var y = 0; y < inner.length; y++) {
475
+ var tk = inner[y], iv = tk.value, it = tk.type;
476
+ if (it === 'punc' && (iv === '(' || iv === '[' || iv === '{')) { dd++; cur.push(tk); }
477
+ else if (it === 'punc' && (iv === ')' || iv === ']' || iv === '}')) { dd--; cur.push(tk); }
478
+ else if (dd === 0 && it === 'punc' && iv === ',') { elems.push(cur); cur = []; }
479
+ else cur.push(tk);
480
+ }
481
+ if (cur.length) elems.push(cur);
482
+ elems = elems.filter(function (e) { return e.length; });
483
+ if (!elems.length) return null;
484
+
485
+ var elem_lines = elems.map(function (e) { return child_indent + render(e, false, '', child_indent, opts, [{ kind: seed_kind }]); });
486
+ var body = elem_lines.join(',\n');
487
+ if (open_ch === '[' || open_ch === '{') body += ','; // magic trailing comma for literals only
488
+
489
+ var tail = code.slice(best.close);
490
+ var tail_str = base_indent + render(tail, false, '', base_indent, opts);
491
+ return head_str + '\n' + body + '\n' + tail_str;
492
+ }
493
+
494
+ // ---------------------------------------------------------------------------
495
+ // Grouping into logical lines / elements
496
+ // ---------------------------------------------------------------------------
497
+
498
+ function make_stmt(tokens) {
499
+ var sig = tokens.filter(function (t) { return t.type !== 'comment' && t.type !== 'shebang'; });
500
+ var first_sig = sig[0], last_sig = sig[sig.length - 1];
501
+ var has_def_or_class = sig.some(function (t) { return t.type === 'keyword' && (t.value === 'def' || t.value === 'class'); });
502
+ var has_semicolon = sig.some(function (t) { return t.value === ';'; });
503
+ // A logical line that starts with `.` or that contains a physical line
504
+ // beginning with `.` is a leading-dot chain (or a do/while `.while` clause).
505
+ // These bind to an outer block by indentation, so we preserve their layout.
506
+ var leading_dot = (first_sig && first_sig.value === '.') ||
507
+ tokens.some(function (t, i) { return i > 0 && t.nlb > 0 && t.type === 'punc' && t.value === '.'; });
508
+ var used_backslash = tokens.some(function (t) { return t.cont; });
509
+ var multi_physical = tokens.some(function (t, i) { return i > 0 && t.nlb > 0; });
510
+ var colon_then_newline = false;
511
+ for (var i = 0; i < tokens.length - 1; i++) { if (tokens[i].value === ':' && tokens[i + 1].nlb > 0) { colon_then_newline = true; break; } }
512
+ var has_multiline_token = tokens.some(function (t) { return typeof t.value === 'string' && t.value.indexOf('\n') >= 0; });
513
+ var interior_comment = false;
514
+ for (var j = 0; j < tokens.length; j++) {
515
+ if (tokens[j].type === 'comment') {
516
+ if (j < tokens.length - 1) interior_comment = true;
517
+ else if (tokens[j].nlb > 0) interior_comment = true;
518
+ }
519
+ }
520
+ var complex = leading_dot || used_backslash || interior_comment || has_def_or_class ||
521
+ has_semicolon || has_multiline_token || (multi_physical && colon_then_newline);
522
+
523
+ var inline_body_colon = false, d = 0;
524
+ for (var s = 0; s < sig.length; s++) {
525
+ var v = sig[s].value, ty = sig[s].type;
526
+ if (ty === 'punc' && (v === '(' || v === '[' || v === '{')) d++;
527
+ else if (ty === 'punc' && (v === ')' || v === ']' || v === '}')) d--;
528
+ else if (d === 0 && v === ':' && s < sig.length - 1) inline_body_colon = true;
529
+ }
530
+ var wrappable = !complex && !has_def_or_class && !has_semicolon && !inline_body_colon;
531
+ var is_def_like = first_sig && ((first_sig.type === 'keyword' && (first_sig.value === 'def' || first_sig.value === 'class')) || first_sig.value === 'async');
532
+ var is_decorator = first_sig && first_sig.value === '@';
533
+ var ends_block = last_sig && last_sig.value === ':';
534
+ return {
535
+ kind: 'stmt', tokens: tokens, indent: tokens[0].line_indent || '',
536
+ blank_before: Math.max(0, tokens[0].nlb - 1), complex: complex, wrappable: wrappable,
537
+ is_def_like: !!is_def_like, is_decorator: !!is_decorator, ends_block: !!ends_block, level: 0,
538
+ };
539
+ }
540
+
541
+ function group(toks) {
542
+ var elements = [];
543
+ var cur = [], depth = 0;
544
+ function flush() { if (cur.length) { elements.push(make_stmt(cur)); cur = []; } }
545
+ for (var k = 0; k < toks.length; k++) {
546
+ var t = toks[k];
547
+ if (t.type === 'eof') break;
548
+ var is_boundary = depth === 0 && cur.length > 0 && t.nlb > 0 && !t.cont && !(t.value === '.' && t.type === 'punc');
549
+ if (is_boundary) flush();
550
+ if (depth === 0 && cur.length === 0 && (t.type === 'comment' || t.type === 'shebang') && (t.nlb > 0 || k === 0)) {
551
+ elements.push({
552
+ kind: 'comment', value: t.value, is_shebang: t.type === 'shebang',
553
+ indent: t.line_indent || '', blank_before: (k === 0 ? 0 : Math.max(0, t.nlb - 1)),
554
+ level: 0, ends_block: false,
555
+ });
556
+ continue;
557
+ }
558
+ cur.push(t);
559
+ if (t.type === 'punc') {
560
+ if (t.value === '(' || t.value === '[' || t.value === '{') depth++;
561
+ else if (t.value === ')' || t.value === ']' || t.value === '}') depth = Math.max(0, depth - 1);
562
+ }
563
+ }
564
+ flush();
565
+ return elements;
566
+ }
567
+
568
+ function compute_levels(elements) {
569
+ var stack = [{ w: -1 }];
570
+ for (var i = 0; i < elements.length; i++) {
571
+ var el = elements[i];
572
+ if (el.kind !== 'stmt') continue;
573
+ var w = el.indent.length;
574
+ if (w > stack[stack.length - 1].w) stack.push({ w: w });
575
+ else {
576
+ while (stack.length > 1 && w < stack[stack.length - 1].w) stack.pop();
577
+ if (w > stack[stack.length - 1].w) stack.push({ w: w });
578
+ }
579
+ el.level = stack.length - 2;
580
+ if (el.level < 0) el.level = 0;
581
+ }
582
+ // Comments take the level of the following statement (or the previous one at EOF).
583
+ for (var c = 0; c < elements.length; c++) {
584
+ if (elements[c].kind !== 'comment') continue;
585
+ var lvl = null;
586
+ for (var f = c + 1; f < elements.length; f++) { if (elements[f].kind === 'stmt') { lvl = elements[f].level; break; } }
587
+ if (lvl === null) { for (var b = c - 1; b >= 0; b--) { if (elements[b].kind === 'stmt') { lvl = elements[b].level; break; } } }
588
+ elements[c].level = lvl === null ? 0 : lvl;
589
+ }
590
+ }
591
+
592
+ // ---------------------------------------------------------------------------
593
+ // Blank line policy helpers
594
+ // ---------------------------------------------------------------------------
595
+
596
+ function leads_def(elements, idx) {
597
+ var j = idx;
598
+ while (j < elements.length) {
599
+ var e = elements[j];
600
+ if (e.kind === 'comment') {
601
+ if (j + 1 < elements.length && elements[j + 1].blank_before === 0) { j++; continue; }
602
+ return false;
603
+ }
604
+ if (e.kind === 'stmt') {
605
+ if (e.is_decorator) { if (j + 1 < elements.length && elements[j + 1].blank_before === 0) { j++; continue; } return false; }
606
+ return !!e.is_def_like;
607
+ }
608
+ return false;
609
+ }
610
+ return false;
611
+ }
612
+
613
+ function is_visual_def_start(elements, idx) {
614
+ if (!leads_def(elements, idx)) return false;
615
+ if (idx === 0) return true;
616
+ var prev = elements[idx - 1], el = elements[idx];
617
+ if (el.blank_before === 0 && (prev.kind === 'comment' || (prev.kind === 'stmt' && prev.is_decorator))) return false;
618
+ return true;
619
+ }
620
+
621
+ // ---------------------------------------------------------------------------
622
+ // Import organization: __python__ group first, then stdlib group, then other group
623
+ // ---------------------------------------------------------------------------
624
+
625
+ // Stdlib module list is generated by self.mjs into dev/stdlib_modules.json by
626
+ // reading src/lib/. Fall back to reading src/lib/ directly when the generated
627
+ // file is absent (e.g. running straight from the release/ tree).
628
+ var STDLIB_MODULES = (function () {
629
+ try {
630
+ return new Set(JSON.parse(fs.readFileSync(
631
+ new URL('../dev/stdlib_modules.json', import.meta.url), 'utf-8')));
632
+ } catch (e) { /* generated file absent; fall through */ }
633
+ try {
634
+ return new Set(fs.readdirSync(fileURLToPath(new URL('../src/lib/', import.meta.url)))
635
+ .filter(function (n) { return n.slice(-4) === '.pyj'; })
636
+ .map(function (n) { return n.slice(0, -4); }));
637
+ } catch (e) { return new Set(); }
638
+ }());
639
+
640
+ function is_import_stmt(el) {
641
+ if (el.kind !== 'stmt') return false;
642
+ var sig = el.tokens.filter(function (t) { return t.type !== 'comment'; });
643
+ if (!sig.length) return false;
644
+ return sig[0].type === 'keyword' && (sig[0].value === 'import' || sig[0].value === 'from');
645
+ }
646
+
647
+ function is_python_builtin_import(el) {
648
+ var sig = el.tokens.filter(function (t) { return t.type !== 'comment'; });
649
+ if (sig.length < 3) return false;
650
+ return sig[0].type === 'keyword' && sig[0].value === 'from' &&
651
+ sig[1].type === 'name' && sig[1].value === '__python__' &&
652
+ sig[2].type === 'keyword' && sig[2].value === 'import';
653
+ }
654
+
655
+ function import_top_module(el) {
656
+ var sig = el.tokens.filter(function (t) { return t.type !== 'comment'; });
657
+ var i = 1; // skip leading 'import' or 'from'
658
+ while (i < sig.length && sig[i].type === 'punc' && sig[i].value === '.') i++; // skip relative dots
659
+ if (i < sig.length && sig[i].type === 'name') return sig[i].value;
660
+ return '';
661
+ }
662
+
663
+ // Returns a string sort key: '0\0<module>\0' for 'import X', '1\0<module>\0<names>' for 'from X import Y'.
664
+ // This puts bare imports before from-imports (isort convention), then sorts alphabetically.
665
+ function import_sort_key(el) {
666
+ var sig = el.tokens.filter(function (t) { return t.type !== 'comment'; });
667
+ if (!sig.length) return '2\0\0';
668
+ var is_from = sig[0].value === 'from';
669
+ var i = 1, dots = '', mod_parts = [];
670
+ while (i < sig.length && sig[i].type === 'punc' && sig[i].value === '.') { dots += '.'; i++; }
671
+ while (i < sig.length && !(sig[i].type === 'keyword' && sig[i].value === 'import')) {
672
+ if (sig[i].type === 'name') mod_parts.push(sig[i].value);
673
+ i++;
674
+ }
675
+ var mod = (dots + mod_parts.join('.')).toLowerCase();
676
+ if (!is_from) return '0\0' + mod + '\0';
677
+ i++; // skip 'import' keyword
678
+ var names = [];
679
+ while (i < sig.length) { if (sig[i].type === 'name') names.push(sig[i].value.toLowerCase()); i++; }
680
+ return '1\0' + mod + '\0' + names.join(',');
681
+ }
682
+
683
+ function organize_import_elements(elements) {
684
+ // Identify the contiguous top-level import block: from the first level-0 import
685
+ // statement to the last, stopping at the first level-0 non-import statement.
686
+ var block_start = -1, block_end = -1;
687
+ for (var i = 0; i < elements.length; i++) {
688
+ var el = elements[i];
689
+ if (el.kind !== 'stmt' || el.level !== 0) continue;
690
+ if (is_import_stmt(el)) {
691
+ if (block_start < 0) block_start = i;
692
+ block_end = i;
693
+ } else {
694
+ if (block_start >= 0) break;
695
+ }
696
+ }
697
+ if (block_start < 0) return elements;
698
+
699
+ var block = elements.slice(block_start, block_end + 1);
700
+ var imports = [], leading_comments = [], seen_import = false;
701
+ for (var j = 0; j < block.length; j++) {
702
+ var bel = block[j];
703
+ if (bel.kind === 'stmt') { seen_import = true; imports.push(bel); }
704
+ else if (bel.kind === 'comment' && !seen_import) leading_comments.push(bel);
705
+ // Comments after the first import are dropped (import organizers conventionally do this).
706
+ }
707
+ if (!imports.length) return elements;
708
+
709
+ var python = [], stdlib = [], other = [];
710
+ for (var k = 0; k < imports.length; k++) {
711
+ if (is_python_builtin_import(imports[k])) python.push(imports[k]);
712
+ else if (STDLIB_MODULES.has(import_top_module(imports[k]))) stdlib.push(imports[k]);
713
+ else other.push(imports[k]);
714
+ }
715
+ function cmp(a, b) { var ka = import_sort_key(a), kb = import_sort_key(b); return ka < kb ? -1 : ka > kb ? 1 : 0; }
716
+ python.sort(cmp);
717
+ stdlib.sort(cmp);
718
+ other.sort(cmp);
719
+
720
+ var new_block = [], first_blank = block[0].blank_before || 0;
721
+ for (var lc = 0; lc < leading_comments.length; lc++) {
722
+ var cm = Object.assign({}, leading_comments[lc]);
723
+ if (lc === 0) cm.blank_before = first_blank;
724
+ new_block.push(cm);
725
+ first_blank = 0;
726
+ }
727
+ for (var p = 0; p < python.length; p++) {
728
+ var pi = Object.assign({}, python[p]);
729
+ pi.blank_before = (p === 0) ? first_blank : 0;
730
+ new_block.push(pi);
731
+ first_blank = 0;
732
+ }
733
+ var has_python = python.length > 0;
734
+ for (var s = 0; s < stdlib.length; s++) {
735
+ var si = Object.assign({}, stdlib[s]);
736
+ si.blank_before = (s === 0 && has_python) ? 1 : (s === 0 ? first_blank : 0);
737
+ new_block.push(si);
738
+ first_blank = 0;
739
+ }
740
+ var has_stdlib_or_python = stdlib.length > 0 || has_python;
741
+ for (var o = 0; o < other.length; o++) {
742
+ var oi = Object.assign({}, other[o]);
743
+ oi.blank_before = (o === 0 && has_stdlib_or_python) ? 1 : (o === 0 ? first_blank : 0);
744
+ new_block.push(oi);
745
+ }
746
+ return elements.slice(0, block_start).concat(new_block).concat(elements.slice(block_end + 1));
747
+ }
748
+
749
+ // ---------------------------------------------------------------------------
750
+ // Element rendering + assembly
751
+ // ---------------------------------------------------------------------------
752
+
753
+ function render_stmt(el, opts) {
754
+ var base_indent = ' '.repeat(4 * el.level);
755
+ if (el.complex) {
756
+ return base_indent + render(el.tokens, true, el.indent, base_indent, opts);
757
+ }
758
+ var comment = null, code = el.tokens;
759
+ if (code.length && code[code.length - 1].type === 'comment') { comment = code[code.length - 1]; code = code.slice(0, -1); }
760
+ var comment_str = comment ? ' ' + format_comment(comment.value) : '';
761
+ if (!opts.join_lines) {
762
+ // Preserve source line breaks unless a resulting line exceeds the limit,
763
+ // in which case fall through to the collapse+wrap path below.
764
+ var preserved = base_indent + render(code, true, el.indent, base_indent, opts);
765
+ var preserved_full = preserved + comment_str;
766
+ var plines = preserved_full.split('\n');
767
+ var fits = true;
768
+ for (var pi = 0; pi < plines.length; pi++) {
769
+ if (plines[pi].length > opts.line_length) { fits = false; break; }
770
+ }
771
+ if (fits) return preserved_full;
772
+ }
773
+ var line = render(code, false, el.indent, base_indent, opts);
774
+ var full = base_indent + line;
775
+ if ((full.length + comment_str.length) > opts.line_length && el.wrappable) {
776
+ var wrapped = wrap_line(code, el.level, opts);
777
+ if (wrapped !== null) return comment_str ? wrapped + comment_str : wrapped;
778
+ }
779
+ return full + comment_str;
780
+ }
781
+
782
+ function render_elements(elements, opts) {
783
+ var result = '';
784
+ for (var idx = 0; idx < elements.length; idx++) {
785
+ var el = elements[idx];
786
+ var etext = (el.kind === 'comment')
787
+ ? (' '.repeat(4 * el.level) + (el.is_shebang ? el.value : format_comment(el.value)))
788
+ : render_stmt(el, opts);
789
+ var blanks = 0;
790
+ if (idx > 0) {
791
+ var prev = elements[idx - 1];
792
+ var cap = (el.level === 0) ? 2 : 1;
793
+ blanks = Math.min(el.blank_before, cap);
794
+ if (prev.ends_block) blanks = 0;
795
+ if (is_visual_def_start(elements, idx)) {
796
+ var req = (el.level === 0) ? 2 : 1;
797
+ if (!prev.ends_block) blanks = Math.max(blanks, req);
798
+ }
799
+ }
800
+ if (idx === 0) result += etext;
801
+ else result += '\n' + '\n'.repeat(blanks) + etext;
802
+ }
803
+ if (result === '') return '';
804
+ return result + '\n';
805
+ }
806
+
807
+ export function format_string(src, options) {
808
+ var opts = normalize_opts(options);
809
+ var text = src.replace(/\r\n?/g, '\n');
810
+ var toks = lex(text);
811
+ var elements = group(toks);
812
+ compute_levels(elements);
813
+ elements = organize_import_elements(elements);
814
+ return render_elements(elements, opts);
815
+ }
816
+
817
+ export function organize_imports(src, options) {
818
+ var opts = normalize_opts(options);
819
+ var text = src.replace(/\r\n?/g, '\n');
820
+ var toks = lex(text);
821
+ var elements = group(toks);
822
+ compute_levels(elements);
823
+ elements = organize_import_elements(elements);
824
+ return render_elements(elements, opts);
825
+ }
826
+
827
+ function normalize_opts(options) {
828
+ options = options || {};
829
+ var ll = options.line_length;
830
+ if (typeof ll === 'string') ll = parseInt(ll, 10);
831
+ if (!ll || isNaN(ll) || ll < 1) ll = 80;
832
+ var pref = options.preferred;
833
+ if (!pref) {
834
+ var pq = options.preferred_quote;
835
+ pref = (pq === 'double' || pq === '"') ? '"' : "'";
836
+ }
837
+ return { line_length: ll, preferred: pref, join_lines: !!options.join_lines };
838
+ }
839
+
840
+ // ---------------------------------------------------------------------------
841
+ // check-only reporting
842
+ // ---------------------------------------------------------------------------
843
+
844
+ export function check_report(file, src, formatted, options) {
845
+ var opts = normalize_opts(options);
846
+ var errs = [];
847
+ if (formatted !== src.replace(/\r\n?/g, '\n')) errs.push(file + ': would be reformatted');
848
+ var lines = formatted.split('\n');
849
+ for (var i = 0; i < lines.length; i++) {
850
+ if (lines[i].length > opts.line_length) {
851
+ errs.push(file + ':' + (i + 1) + ': line exceeds ' + opts.line_length + ' characters (' + lines[i].length + ')');
852
+ }
853
+ }
854
+ return errs;
855
+ }
856
+
857
+ // ---------------------------------------------------------------------------
858
+ // File collection + CLI
859
+ // ---------------------------------------------------------------------------
860
+
861
+ export async function collect_pyj_files(inputs) {
862
+ var files = [];
863
+ async function walk(list, from_dir) {
864
+ for (var i = 0; i < list.length; i++) {
865
+ var f = list[i];
866
+ var st;
867
+ try { st = await fs.promises.lstat(f); }
868
+ catch (e) {
869
+ if (from_dir && (e.code === 'EACCES' || e.code === 'EPERM')) continue;
870
+ throw new Error("can't access: " + f);
871
+ }
872
+ if (st.isDirectory()) {
873
+ var children;
874
+ try {
875
+ children = (await fs.promises.readdir(f)).sort().map(function (x) { return path.join(f, x); });
876
+ } catch (e) {
877
+ if (from_dir && (e.code === 'EACCES' || e.code === 'EPERM')) continue;
878
+ throw new Error("can't read directory: " + f);
879
+ }
880
+ await walk(children, true);
881
+ } else if (st.isFile()) {
882
+ if (from_dir) { if (f.endsWith('.pyj')) files.push(f); }
883
+ else files.push(f);
884
+ }
885
+ }
886
+ }
887
+ await walk(inputs, false);
888
+ return files;
889
+ }
890
+
891
+ async function read_stdin() {
892
+ var chunks = [];
893
+ process.stdin.setEncoding('utf-8');
894
+ await new Promise(function (resolve, reject) {
895
+ process.stdin.on('data', function (chunk) { chunks.push(chunk); });
896
+ process.stdin.on('end', resolve);
897
+ process.stdin.on('error', reject);
898
+ });
899
+ return chunks.join('');
900
+ }
901
+
902
+ function argv_has_flag(flag_names) {
903
+ var args = process.argv.slice(3); // skip: node, script path, mode
904
+ for (var i = 0; i < args.length; i++) {
905
+ var a = args[i];
906
+ if (a === '--') break;
907
+ for (var j = 0; j < flag_names.length; j++) {
908
+ var f = flag_names[j];
909
+ if (a === f || a.startsWith(f + '=')) return true;
910
+ }
911
+ }
912
+ return false;
913
+ }
914
+
915
+ export async function cli(argv, base_path, src_path, lib_path) {
916
+ var ll_from_cli = argv_has_flag(['--line-length', '--line_length', '-l']);
917
+ var q_from_cli = argv_has_flag(['--preferred-quote', '--preferred_quote', '-q']);
918
+
919
+ var effective_ll = ll_from_cli ? (parseInt(argv.line_length, 10) || 80) : null;
920
+ var effective_quote = q_from_cli ? argv.preferred_quote : null;
921
+
922
+ if (!ll_from_cli || !q_from_cli) {
923
+ var pyconf = await read_pyproject_config(process.cwd());
924
+ if (!ll_from_cli && pyconf.line_length) effective_ll = pyconf.line_length;
925
+ if (!q_from_cli && pyconf.preferred_quote) effective_quote = pyconf.preferred_quote;
926
+ }
927
+
928
+ if (!effective_ll) effective_ll = 80;
929
+ if (!effective_quote) effective_quote = 'single';
930
+
931
+ var opts = normalize_opts({ line_length: effective_ll, preferred_quote: effective_quote, join_lines: argv.join_lines });
932
+ var inputs = (argv.files || []).slice();
933
+
934
+ if (inputs.length === 0) {
935
+ var src = await read_stdin();
936
+ var out;
937
+ try { out = format_string(src, opts); }
938
+ catch (e) { console.error('Error formatting stdin: ' + (e.message || e)); process.exit(2); }
939
+ process.stdout.write(out);
940
+ process.exit(0);
941
+ }
942
+
943
+ var files;
944
+ try { files = await collect_pyj_files(inputs); }
945
+ catch (e) { console.error('ERROR: ' + (e.message || e)); process.exit(2); }
946
+
947
+ var had_errors = false;
948
+ for (var i = 0; i < files.length; i++) {
949
+ var f = files[i];
950
+ var code;
951
+ try { code = await fs.promises.readFile(f, 'utf-8'); }
952
+ catch (e) { console.error("ERROR: can't read file: " + f); process.exit(2); }
953
+ var formatted;
954
+ try { formatted = format_string(code, opts); }
955
+ catch (e) { console.error(f + ': ' + (e.message || e)); had_errors = true; continue; }
956
+ if (argv.check_only) {
957
+ var errs = check_report(f, code, formatted, opts);
958
+ if (errs.length) { had_errors = true; errs.forEach(function (m) { console.error(m); }); }
959
+ } else {
960
+ if (formatted !== code.replace(/\r\n?/g, '\n')) {
961
+ try { await fs.promises.writeFile(f, formatted); }
962
+ catch (e) { console.error("ERROR: can't write file: " + f); process.exit(2); }
963
+ console.log('reformatted ' + f);
964
+ }
965
+ var flines = formatted.split('\n');
966
+ for (var li = 0; li < flines.length; li++) {
967
+ if (flines[li].length > opts.line_length) {
968
+ console.error(f + ':' + (li + 1) + ': line exceeds ' + opts.line_length + ' characters (' + flines[li].length + ')');
969
+ had_errors = true;
970
+ }
971
+ }
972
+ }
973
+ }
974
+ process.exit(had_errors ? 1 : 0);
975
+ }