coffeehaml 0.7.0 → 0.7.2

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 (62) hide show
  1. package/README.md +21 -5
  2. package/dist/ast.js +334 -125
  3. package/dist/cli.js +363 -291
  4. package/dist/compiler.js +126 -89
  5. package/dist/emitter.js +771 -736
  6. package/dist/errors.js +5 -2
  7. package/dist/expressions.js +211 -154
  8. package/dist/index.js +22 -6
  9. package/dist/lexer.js +536 -340
  10. package/dist/parser.js +533 -438
  11. package/dist/prettier/coffeescript-formatter.js +292 -392
  12. package/dist/prettier/index.js +96 -60
  13. package/dist/prettier/options.js +179 -147
  14. package/dist/prettier/printer.js +549 -642
  15. package/dist/types.js +122 -12
  16. package/dist/vite-plugin.js +78 -63
  17. package/package.json +4 -9
  18. package/dist/ast.d.ts +0 -123
  19. package/dist/ast.d.ts.map +0 -1
  20. package/dist/ast.js.map +0 -1
  21. package/dist/cli.d.ts +0 -3
  22. package/dist/cli.d.ts.map +0 -1
  23. package/dist/cli.js.map +0 -1
  24. package/dist/compiler.d.ts +0 -10
  25. package/dist/compiler.d.ts.map +0 -1
  26. package/dist/compiler.js.map +0 -1
  27. package/dist/emitter.d.ts +0 -4
  28. package/dist/emitter.d.ts.map +0 -1
  29. package/dist/emitter.js.map +0 -1
  30. package/dist/errors.d.ts +0 -3
  31. package/dist/errors.d.ts.map +0 -1
  32. package/dist/errors.js.map +0 -1
  33. package/dist/expressions.d.ts +0 -22
  34. package/dist/expressions.d.ts.map +0 -1
  35. package/dist/expressions.js.map +0 -1
  36. package/dist/index.d.ts +0 -7
  37. package/dist/index.d.ts.map +0 -1
  38. package/dist/index.js.map +0 -1
  39. package/dist/lexer.d.ts +0 -30
  40. package/dist/lexer.d.ts.map +0 -1
  41. package/dist/lexer.js.map +0 -1
  42. package/dist/parser.d.ts +0 -10
  43. package/dist/parser.d.ts.map +0 -1
  44. package/dist/parser.js.map +0 -1
  45. package/dist/prettier/coffeescript-formatter.d.ts +0 -31
  46. package/dist/prettier/coffeescript-formatter.d.ts.map +0 -1
  47. package/dist/prettier/coffeescript-formatter.js.map +0 -1
  48. package/dist/prettier/index.d.ts +0 -8
  49. package/dist/prettier/index.d.ts.map +0 -1
  50. package/dist/prettier/index.js.map +0 -1
  51. package/dist/prettier/options.d.ts +0 -10
  52. package/dist/prettier/options.d.ts.map +0 -1
  53. package/dist/prettier/options.js.map +0 -1
  54. package/dist/prettier/printer.d.ts +0 -45
  55. package/dist/prettier/printer.d.ts.map +0 -1
  56. package/dist/prettier/printer.js.map +0 -1
  57. package/dist/types.d.ts +0 -76
  58. package/dist/types.d.ts.map +0 -1
  59. package/dist/types.js.map +0 -1
  60. package/dist/vite-plugin.d.ts +0 -7
  61. package/dist/vite-plugin.d.ts.map +0 -1
  62. package/dist/vite-plugin.js.map +0 -1
@@ -1,409 +1,309 @@
1
- /**
2
- * CoffeeScript Token-Based Formatter
3
- *
4
- * Normalizes whitespace and wraps long lines in CoffeeScript expressions
5
- * embedded within CoffeeHaml templates. Uses CoffeeScript's own lexer
6
- * (`CoffeeScript.tokens()`) for robust tokenization. Falls back to
7
- * original source on any parse error.
8
- *
9
- * Approach:
10
- * Phase 1: Tokenize with CoffeeScript's lexer
11
- * Phase 2: Normalize spacing based on token categories
12
- * Phase 3: Line-wrap expressions exceeding printWidth
13
- * Fallback: Return original source on any error
14
- */
15
- import { createRequire } from 'module';
16
- const _require = createRequire(import.meta.url);
17
- function loadCoffeeScript() {
18
- try {
19
- return _require('coffeescript');
20
- }
21
- catch {
22
- return null;
23
- }
24
- }
1
+ // Generated by CoffeeScript 2.7.0
2
+ // coffeescript-formatter.coffee — CoffeeScript token-based expression formatter
3
+ var DEFAULT_OPTS, EXPR_STARTERS, FOR_KEYWORDS, KEYWORD_SPACE_AFTER, NO_SPACE_AFTER, NO_SPACE_BEFORE, SPACE_AFTER, SPACE_BOTH, _require, getTokenText, hasImplicitTokens, hasMethodChain, isUnaryOp, lineWrap, loadCoffeeScript, needsSpaceBetween, normalizeSpacing, wrapMethodChain;
4
+
5
+ import {
6
+ createRequire
7
+ } from 'module';
8
+
9
+ _require = createRequire(import.meta.url);
10
+
11
+ loadCoffeeScript = function() {
12
+ try {
13
+ return _require('coffeescript');
14
+ } catch (error) {
15
+ return null;
16
+ }
17
+ };
18
+
19
+ // ─── Token types ──────────────────────────────────────────
20
+ // CSToken: { type, value, range, generated?, originalText? }
21
+
25
22
  // ─── Spacing categories ───────────────────────────────────
26
- // ──────────────────────────────────────────────────────────
27
- // NOTE: Spacing rules use CoffeeScript token TYPES, not values.
28
- // CoffeeScript categorizes operators into type groups:
29
- // * / % → MATH
30
- // << >> >>> → SHIFT
31
- // == != < > → COMPARE
32
- // is isnt → COMPARE (rewritten to ==, !=)
33
- // and or → &&, || (rewritten)
34
- // not → UNARY (rewritten to !)
35
- // += -= etc → COMPOUND_ASSIGN
36
- // + - → + - (same type as value)
37
- // We use original source text for output but token types for spacing.
38
- // ──────────────────────────────────────────────────────────
39
- /** Tokens that should NOT have a space before them. */
40
- const NO_SPACE_BEFORE = new Set([
41
- ',', ':', ';', '.', '?.', '::',
42
- ')', ']', '}',
43
- 'CALL_END', 'PARAM_END', 'INDEX_END',
44
- 'POST_IF', 'POST_FOR', 'POST_WHILE',
45
- 'TERMINATOR',
46
- // NOTE: FORIN, FOROF removed — they need space before them
47
- ]);
48
- /** Tokens that should NOT have a space after them. */
49
- const NO_SPACE_AFTER = new Set([
50
- '.', '?.', '::', '...', '..',
51
- '(', '[', '{',
52
- 'CALL_START', 'PARAM_START', 'INDEX_START',
53
- 'STRING_START', 'INTERPOLATION_START',
54
- 'INDENT', '@',
55
- // UNARY removed — we handle it in needsSpaceBetween based on original text
56
- ]);
57
- /** Tokens that ALWAYS get a space after them. */
58
- const SPACE_AFTER = new Set([
59
- ',', // comma in args/arrays
60
- ':', // colon in object literals
61
- ]);
62
- /** Binary operators that get spaces on both sides.
63
- * Uses CoffeeScript token TYPES (MATH, SHIFT, COMPARE, etc.). */
64
- const SPACE_BOTH = new Set([
65
- '=', '?=',
66
- '+', '-', // simple arithmetic (unary handled separately)
67
- 'MATH', // * / %
68
- '**', // power
69
- 'SHIFT', // << >> >>>
70
- '&', '|', '^', // bitwise
71
- '&&', '||', '?', // logical
72
- '=>', '->', // arrows
73
- 'COMPARE', // == != < > <= >= is isnt
74
- 'COMPOUND_ASSIGN', // += -= *= /= etc.
75
- 'RELATION', // in, of (when not FORIN/FOROF)
76
- 'FOR', // for (comprehension keyword)
77
- 'ELSE', 'IF', 'WHILE', // control flow keywords
78
- ]);
79
- /** Keywords that should have a space after them (but not before). */
80
- const KEYWORD_SPACE_AFTER = new Set([
81
- 'RETURN', 'THROW', 'NEW', 'TYPEOF', 'INSTANCEOF', 'DELETE',
82
- 'CLASS', 'EXTENDS', 'DEFAULT', 'IMPORT', 'EXPORT', 'AWAIT',
83
- 'YIELD', 'BREAK', 'CONTINUE', 'SWITCH',
84
- ]);
85
- /** For-comprehension internal keywords get space on both sides. */
86
- const FOR_KEYWORDS = new Set(['FORIN', 'FOROF', 'WHEN', 'BY']);
87
- /** Tokens that start a new expression — after these, - and + are unary. */
88
- const EXPR_STARTERS = new Set([
89
- '(', '[', '{', ',', ':',
90
- '=', '?=',
91
- '+', '-', 'MATH', '**', 'SHIFT',
92
- '&', '|', '^',
93
- '&&', '||', '?',
94
- '=>', '->',
95
- 'COMPARE', 'COMPOUND_ASSIGN',
96
- 'RETURN', 'INDENT', 'OUTDENT', 'TERMINATOR', 'FOR', 'FORIN', 'FOROF',
97
- 'WHILE', 'IF', 'ELSE', 'THROW', 'NEW', 'TYPEOF', 'INSTANCEOF',
98
- 'DELETE', 'EXTENDS', 'DEFAULT',
99
- 'CALL_START', 'PARAM_START', 'INDEX_START',
100
- 'UNARY',
101
- ]);
102
- const DEFAULT_OPTS = {
103
- printWidth: 80,
104
- methodChainAlign: true,
105
- enabled: true,
23
+ NO_SPACE_BEFORE = new Set([',', ':', ';', '.', '?.', '::', ')', ']', '}', 'CALL_END', 'PARAM_END', 'INDEX_END', 'POST_IF', 'POST_FOR', 'POST_WHILE', 'TERMINATOR']);
24
+
25
+ NO_SPACE_AFTER = new Set(['.', '?.', '::', '...', '..', '(', '[', '{', 'CALL_START', 'PARAM_START', 'INDEX_START', 'STRING_START', 'INTERPOLATION_START', 'INDENT', '@']);
26
+
27
+ SPACE_AFTER = new Set([',', ':']);
28
+
29
+ SPACE_BOTH = new Set(['=', '?=', '+', '-', 'MATH', '**', 'SHIFT', '&', '|', '^', '&&', '||', '?', '=>', '->', 'COMPARE', 'COMPOUND_ASSIGN', 'RELATION', 'FOR', 'ELSE', 'IF', 'WHILE']);
30
+
31
+ KEYWORD_SPACE_AFTER = new Set(['RETURN', 'THROW', 'NEW', 'TYPEOF', 'INSTANCEOF', 'DELETE', 'CLASS', 'EXTENDS', 'DEFAULT', 'IMPORT', 'EXPORT', 'AWAIT', 'YIELD', 'BREAK', 'CONTINUE', 'SWITCH']);
32
+
33
+ FOR_KEYWORDS = new Set(['FORIN', 'FOROF', 'WHEN', 'BY']);
34
+
35
+ EXPR_STARTERS = new Set(['(', '[', '{', ',', ':', '=', '?=', '+', '-', 'MATH', '**', 'SHIFT', '&', '|', '^', '&&', '||', '?', '=>', '->', 'COMPARE', 'COMPOUND_ASSIGN', 'RETURN', 'INDENT', 'OUTDENT', 'TERMINATOR', 'FOR', 'FORIN', 'FOROF', 'WHILE', 'IF', 'ELSE', 'THROW', 'NEW', 'TYPEOF', 'INSTANCEOF', 'DELETE', 'EXTENDS', 'DEFAULT', 'CALL_START', 'PARAM_START', 'INDEX_START', 'UNARY']);
36
+
37
+ DEFAULT_OPTS = {
38
+ printWidth: 80,
39
+ methodChainAlign: true,
40
+ enabled: true
106
41
  };
42
+
107
43
  // ─── Public API ───────────────────────────────────────────
108
- /**
109
- * Format a CoffeeScript expression string.
110
- * Returns the formatted string or the original on error.
111
- */
112
- export function formatCoffeeScript(source, options = {}) {
113
- const opts = { ...DEFAULT_OPTS, ...options };
114
- if (!opts.enabled)
115
- return source;
116
- if (!source || source.trim() === '')
117
- return source;
44
+ export var formatCoffeeScript = function(source, options = {}) {
45
+ var cs, normalized, opts, rawTokens, tokens;
46
+ opts = {...DEFAULT_OPTS, ...options};
47
+ if (!opts.enabled) {
48
+ return source;
49
+ }
50
+ if (!(source && source.trim() !== '')) {
51
+ return source;
52
+ }
53
+ try {
54
+ cs = loadCoffeeScript();
55
+ if (!(cs && typeof cs.tokens === 'function')) {
56
+ return source;
57
+ }
58
+ rawTokens = cs.tokens(source);
59
+ if (!(rawTokens && rawTokens.length > 0)) {
60
+ return source;
61
+ }
62
+ tokens = rawTokens.map(function(t) {
63
+ var ref, ref1, ref2, ref3;
64
+ return {
65
+ type: t[0],
66
+ value: t[1],
67
+ range: (ref = (ref1 = t[2]) != null ? ref1.range : void 0) != null ? ref : [0, 0],
68
+ generated: !!((ref2 = t[2]) != null ? ref2.generated : void 0),
69
+ originalText: ((ref3 = t[2]) != null ? ref3.range : void 0) && t[2].range[0] < t[2].range[1] ? source.slice(t[2].range[0], t[2].range[1]) : t[1]
70
+ };
71
+ });
72
+ if (hasImplicitTokens(tokens)) {
73
+ return source;
74
+ }
75
+ normalized = normalizeSpacing(tokens, source);
76
+ if (normalized.length > opts.printWidth) {
77
+ return lineWrap(normalized, opts, source);
78
+ }
79
+ return normalized;
80
+ } catch (error) {
81
+ return source;
82
+ }
83
+ };
84
+
85
+ // ─── Block formatting ─────────────────────────────────────
86
+ export var formatCoffeeScriptBlock = function(source, options = {}) {
87
+ var formatted, lines, opts;
88
+ opts = {...DEFAULT_OPTS, ...options};
89
+ if (!opts.enabled) {
90
+ return source;
91
+ }
92
+ if (!(source && source.trim() !== '')) {
93
+ return source;
94
+ }
95
+ lines = source.split('\n');
96
+ formatted = lines.map(function(line) {
97
+ var formattedExpr, leading, ref, ref1, trimmed;
98
+ trimmed = line.trim();
99
+ if (!trimmed) {
100
+ return '';
101
+ }
102
+ leading = (ref = (ref1 = line.match(/^(\s*)/)) != null ? ref1[1] : void 0) != null ? ref : '';
118
103
  try {
119
- const cs = loadCoffeeScript();
120
- if (!cs || typeof cs.tokens !== 'function')
121
- return source;
122
- // CoffeeScript.tokens() returns tuples: [string, string, location]
123
- // Map them to our CSToken interface for easier access
124
- const rawTokens = cs.tokens(source);
125
- if (!rawTokens || rawTokens.length === 0)
126
- return source;
127
- const tokens = rawTokens.map((t) => ({
128
- type: t[0],
129
- value: t[1],
130
- range: t[2]?.range ?? [0, 0],
131
- generated: !!t[2]?.generated,
132
- originalText: (t[2]?.range && t[2].range[0] < t[2].range[1])
133
- ? source.slice(t[2].range[0], t[2].range[1])
134
- : t[1],
135
- }));
136
- // Check for implicit/synthetic tokens that would make formatting unsafe.
137
- // If the expression has zero-width tokens (implicit calls, generated params),
138
- // fall back to original source to avoid changing semantics.
139
- if (hasImplicitTokens(tokens)) {
140
- return source;
141
- }
142
- // Phase 2: Normalize spacing (pass original source for text extraction)
143
- const normalized = normalizeSpacing(tokens, source);
144
- // Phase 3: Line-wrap if needed
145
- if (normalized.length > opts.printWidth) {
146
- return lineWrap(normalized, opts, source);
147
- }
148
- return normalized;
104
+ formattedExpr = formatCoffeeScript(trimmed, options);
105
+ return leading + formattedExpr;
106
+ } catch (error) {
107
+ return line;
149
108
  }
150
- catch (e) {
151
- // On any error, return original source unchanged
152
- return source;
109
+ });
110
+ return formatted.join('\n');
111
+ };
112
+
113
+ // ─── Implicit token detection ─────────────────────────────
114
+ hasImplicitTokens = function(tokens) {
115
+ var j, len, ref, t;
116
+ for (j = 0, len = tokens.length; j < len; j++) {
117
+ t = tokens[j];
118
+ if ((ref = t.type) === 'INDENT' || ref === 'OUTDENT' || ref === 'TERMINATOR') {
119
+ continue;
153
120
  }
154
- }
155
- /**
156
- * Check if the token stream contains implicit/synthetic tokens
157
- * (zero-width CALL/PARAM tokens) that indicate CoffeeScript
158
- * implicit function calls. Formatting these would change semantics.
159
- */
160
- function hasImplicitTokens(tokens) {
161
- for (const t of tokens) {
162
- if (t.type === 'INDENT' || t.type === 'OUTDENT' || t.type === 'TERMINATOR')
163
- continue;
164
- if (t.generated)
165
- return true;
166
- if (t.range && t.range[0] >= t.range[1])
167
- return true;
168
- // Check for CALL_START/CALL_END that aren't explicit parens:
169
- // If they're at same position as another token, they're implicit
121
+ if (t.generated) {
122
+ return true;
170
123
  }
171
- return false;
172
- }
173
- // ─── Phase 2: Spacing normalization ──────────────────────
174
- /**
175
- * Normalize whitespace in a token stream.
176
- * Uses original source text (from token ranges) for output
177
- * but CoffeeScript token TYPES for spacing decisions.
178
- * This preserves keywords like 'is', 'and', 'not' that
179
- * CoffeeScript rewrites internally.
180
- */
181
- function normalizeSpacing(tokens, originalSource) {
182
- const parts = [];
183
- let prevVisible = null;
184
- // Track whether prev was at expression start (for unary detection).
185
- // Starts true because the first token is at expression start.
186
- let prevWasAtExprStart = true;
187
- // Track whether the position AFTER prev starts an expression
188
- // (for the next token's unary detection).
189
- let afterPrevIsExprStart = true;
190
- for (let i = 0; i < tokens.length; i++) {
191
- const cur = tokens[i];
192
- // Skip implicit/synthetic call tokens:
193
- // - generated CALL_START/CALL_END
194
- // - zero-width CALL_START/CALL_END (CoffeeScript may not mark these as generated)
195
- if (cur.type === 'CALL_START' || cur.type === 'CALL_END') {
196
- if (cur.generated)
197
- continue;
198
- // Also skip if zero-width (implicit parens)
199
- if (cur.range && cur.range[0] >= cur.range[1])
200
- continue;
201
- }
202
- // Also skip zero-width PARAM_START/PARAM_END (generated arrow params)
203
- if (cur.type === 'PARAM_START' || cur.type === 'PARAM_END') {
204
- if (cur.generated)
205
- continue;
206
- if (cur.range && cur.range[0] >= cur.range[1])
207
- continue;
208
- }
209
- // INDENT/OUTDENT: skip output but track the previous real token
210
- // for spacing purposes (INDENT after => should not break the
211
- // space between => and the next token).
212
- if (cur.type === 'INDENT' || cur.type === 'OUTDENT') {
213
- // Don't update prevVisible — keep the last real token
214
- continue;
215
- }
216
- // TERMINATOR: skip entirely
217
- if (cur.type === 'TERMINATOR') {
218
- continue;
219
- }
220
- // Determine space before this token
221
- if (prevVisible && prevVisible.type !== 'INDENT' && prevVisible.type !== 'OUTDENT') {
222
- if (needsSpaceBetween(prevVisible, cur, prevWasAtExprStart)) {
223
- parts.push(' ');
224
- }
225
- }
226
- // Output token text: prefer original source, fall back to token value
227
- parts.push(getTokenText(cur, originalSource));
228
- // Update tracking for next iteration
229
- prevWasAtExprStart = afterPrevIsExprStart;
230
- afterPrevIsExprStart = EXPR_STARTERS.has(cur.type);
124
+ if (t.range && t.range[0] >= t.range[1]) {
125
+ return true;
126
+ }
127
+ }
128
+ return false;
129
+ };
130
+
131
+ // ─── Spacing normalization ────────────────────────────────
132
+ normalizeSpacing = function(tokens, originalSource = null) {
133
+ var afterPrevIsExprStart, cur, j, len, parts, prevVisible, prevWasAtExprStart, ref, ref1, ref2, text, trimmed;
134
+ parts = [];
135
+ prevVisible = null;
136
+ prevWasAtExprStart = true;
137
+ afterPrevIsExprStart = true;
138
+ for (j = 0, len = tokens.length; j < len; j++) {
139
+ cur = tokens[j];
140
+ // Skip implicit CALL/PARAM tokens
141
+ if ((ref = cur.type) === 'CALL_START' || ref === 'CALL_END') {
142
+ if (cur.generated) {
143
+ continue;
144
+ }
145
+ if (cur.range && cur.range[0] >= cur.range[1]) {
146
+ continue;
147
+ }
148
+ }
149
+ if ((ref1 = cur.type) === 'PARAM_START' || ref1 === 'PARAM_END') {
150
+ if (cur.generated) {
151
+ continue;
152
+ }
153
+ if (cur.range && cur.range[0] >= cur.range[1]) {
154
+ continue;
155
+ }
156
+ }
157
+ // Handle INDENT — represents inline then/when/else/etc from CoffeeScript
158
+ if (cur.type === 'INDENT') {
159
+ text = getTokenText(cur, originalSource);
160
+ trimmed = text != null ? text.trim() : void 0;
161
+ if (trimmed && trimmed !== '') {
162
+ parts.push(' ' + trimmed + ' ');
163
+ prevWasAtExprStart = false;
164
+ afterPrevIsExprStart = true;
231
165
  prevVisible = cur;
166
+ }
167
+ continue;
232
168
  }
233
- return parts.join('');
234
- }
235
- /**
236
- * Get the original source text for a token.
237
- * CoffeeScript may rewrite token values (is→==, and→&&),
238
- * so we use the original source range when available.
239
- */
240
- /**
241
- * Get the output text for a token.
242
- * Prefers original source text from the token's range,
243
- * but falls back to token value for zero-width ranges,
244
- * generated tokens, or when originalSource is unavailable.
245
- */
246
- function getTokenText(token, originalSource) {
247
- // For generated tokens, use the token value (e.g., '(' for CALL_START)
248
- if (token.generated)
249
- return token.value;
250
- // For zero-width ranges, use token value
251
- if (!token.range || token.range[0] >= token.range[1])
252
- return token.value;
253
- // Use original source text
254
- if (originalSource) {
255
- return originalSource.slice(token.range[0], token.range[1]);
169
+ if (cur.type === 'OUTDENT') {
170
+ continue;
256
171
  }
257
- return token.value;
258
- }
259
- /**
260
- * Determine if a space is needed between two visible tokens.
261
- * @param prevAtExprStart true if the position BEFORE prev was at
262
- * expression start — used to detect unary + / - operators.
263
- */
264
- function needsSpaceBetween(prev, cur, prevAtExprStart) {
265
- // No space after these token types (e.g., ., (, [, @)
266
- if (NO_SPACE_AFTER.has(prev.type))
267
- return false;
268
- // No space before these token types (e.g., , : ; . ) ] })
269
- if (NO_SPACE_BEFORE.has(cur.type))
270
- return false;
271
- // @property access: no space after @
272
- if (prev.type === '@')
273
- return false;
274
- // Tokens that always get a space after (comma, colon)
275
- if (SPACE_AFTER.has(prev.type))
276
- return true;
277
- // For-comprehension keywords: space around FORIN, FOROF, WHEN, BY
278
- if (FOR_KEYWORDS.has(prev.type) || FOR_KEYWORDS.has(cur.type))
279
- return true;
280
- // Keywords that take space after them (FOR, RETURN, THROW, etc.)
281
- if (KEYWORD_SPACE_AFTER.has(prev.type))
282
- return true;
283
- // UNARY (! or not): no space after if the original text is !
284
- // (as opposed to the keyword 'not')
285
- if (prev.type === 'UNARY') {
286
- const text = prev.originalText || prev.value;
287
- if (text === '!' || text === '!!')
288
- return false;
289
- // For 'not' keyword: space after is desired
290
- return true;
172
+ // Skip TERMINATOR
173
+ if (cur.type === 'TERMINATOR') {
174
+ continue;
291
175
  }
292
- // Binary operators: space on both sides
293
- if (SPACE_BOTH.has(prev.type) || SPACE_BOTH.has(cur.type)) {
294
- // If prev is + or - and is at expression start, it's unary — no space after it
295
- if (isUnaryOp(prev, prevAtExprStart))
296
- return false;
297
- return true;
176
+ // Space before this token
177
+ if (prevVisible && ((ref2 = prevVisible.type) !== 'INDENT' && ref2 !== 'OUTDENT')) {
178
+ if (needsSpaceBetween(prevVisible, cur, prevWasAtExprStart)) {
179
+ parts.push(' ');
180
+ }
298
181
  }
299
- // Space between two identifiers (rare but possible)
300
- if (prev.type === 'IDENTIFIER' && cur.type === 'IDENTIFIER')
301
- return true;
302
- // Default: no extra space
182
+ parts.push(getTokenText(cur, originalSource));
183
+ prevWasAtExprStart = afterPrevIsExprStart;
184
+ afterPrevIsExprStart = EXPR_STARTERS.has(cur.type);
185
+ prevVisible = cur;
186
+ }
187
+ return parts.join('');
188
+ };
189
+
190
+ getTokenText = function(token, originalSource = null) {
191
+ if (token.generated) {
192
+ return token.value;
193
+ }
194
+ if (!(token.range && token.range[0] < token.range[1])) {
195
+ return token.value;
196
+ }
197
+ if (originalSource) {
198
+ return originalSource.slice(token.range[0], token.range[1]);
199
+ }
200
+ return token.value;
201
+ };
202
+
203
+ needsSpaceBetween = function(prev, cur, prevAtExprStart) {
204
+ var text;
205
+ if (NO_SPACE_AFTER.has(prev.type)) {
206
+ return false;
207
+ }
208
+ if (NO_SPACE_BEFORE.has(cur.type)) {
303
209
  return false;
304
- }
305
- /**
306
- * Check if prev token is a unary + or - operator.
307
- * prevAtExprStart is the expression-start state BEFORE prev was processed.
308
- */
309
- function isUnaryOp(prev, prevAtExprStart) {
310
- if (prev.type !== '+' && prev.type !== '-')
311
- return false;
312
- // At expression start (first token or after expression starter)
313
- if (prevAtExprStart)
314
- return true;
315
- // Also unary if at the very beginning (range[0] === 0)
316
- if (prev.range && prev.range[0] === 0)
317
- return true;
210
+ }
211
+ if (prev.type === '@') {
212
+ return false;
213
+ }
214
+ if (SPACE_AFTER.has(prev.type)) {
215
+ return true;
216
+ }
217
+ if (FOR_KEYWORDS.has(prev.type) || FOR_KEYWORDS.has(cur.type)) {
218
+ return true;
219
+ }
220
+ if (KEYWORD_SPACE_AFTER.has(prev.type)) {
221
+ return true;
222
+ }
223
+ if (prev.type === 'UNARY') {
224
+ text = prev.originalText || prev.value;
225
+ if (text === '!' || text === '!!') {
226
+ return false;
227
+ }
228
+ return true;
229
+ }
230
+ if (SPACE_BOTH.has(prev.type) || SPACE_BOTH.has(cur.type)) {
231
+ if (isUnaryOp(prev, prevAtExprStart)) {
232
+ return false;
233
+ }
234
+ return true;
235
+ }
236
+ if (prev.type === 'IDENTIFIER' && cur.type === 'IDENTIFIER') {
237
+ return true;
238
+ }
239
+ return false;
240
+ };
241
+
242
+ isUnaryOp = function(prev, prevAtExprStart) {
243
+ var ref;
244
+ if ((ref = prev.type) !== '+' && ref !== '-') {
318
245
  return false;
319
- }
320
- // ─── Phase 3: Line wrapping ──────────────────────────────
321
- function lineWrap(source, opts, fallback) {
322
- // Strategy: break at natural points
323
- // 1. Method chains: break after each .method
324
- // 2. Arrow functions: break after =>
325
- // 3. Binary expressions: break after operators
326
- // 4. Object literals: break after ,
327
- // For now, implement method chain wrapping (most common long-expression case)
328
- if (opts.methodChainAlign && hasMethodChain(source)) {
329
- return wrapMethodChain(source, opts);
246
+ }
247
+ if (prevAtExprStart) {
248
+ return true;
249
+ }
250
+ if (prev.range && prev.range[0] === 0) {
251
+ return true;
252
+ }
253
+ return false;
254
+ };
255
+
256
+ // ─── Line wrapping ────────────────────────────────────────
257
+ lineWrap = function(source, opts, fallback) {
258
+ if (opts.methodChainAlign && hasMethodChain(source)) {
259
+ return wrapMethodChain(source, opts);
260
+ }
261
+ return fallback;
262
+ };
263
+
264
+ hasMethodChain = function(source) {
265
+ return (source.match(/\.[a-zA-Z_$]/g) || []).length >= 2;
266
+ };
267
+
268
+ wrapMethodChain = function(source, _opts) {
269
+ var ch, chars, current, depth, i, indent, result;
270
+ indent = ' ';
271
+ result = [];
272
+ chars = [...source];
273
+ depth = 0;
274
+ current = '';
275
+ i = 0;
276
+ while (i < chars.length) {
277
+ ch = chars[i];
278
+ if (ch === '(' || ch === '[' || ch === '{') {
279
+ depth++;
330
280
  }
331
- // Fall back — source is already normalized, just too long
332
- return fallback;
333
- }
334
- function hasMethodChain(source) {
335
- // Detect .method chains (not just a single .property)
336
- const dotCount = (source.match(/\.[a-zA-Z_$]/g) || []).length;
337
- return dotCount >= 2;
338
- }
339
- function wrapMethodChain(source, _opts) {
340
- // Break .method calls onto separate lines with aligned dots
341
- // But be careful not to break inside strings, parens, or brackets
342
- const indent = ' '.repeat(1); // 1 level of indentation for continuation
343
- const result = [];
344
- const chars = [...source];
345
- let depth = 0;
346
- let current = '';
347
- let i = 0;
348
- while (i < chars.length) {
349
- const ch = chars[i];
350
- // Track nesting depth
351
- if (ch === '(' || ch === '[' || ch === '{')
352
- depth++;
353
- if (ch === ')' || ch === ']' || ch === '}')
354
- depth--;
355
- // Break before . at depth 0
356
- if (ch === '.' && depth === 0 && i > 0 && chars[i - 1] !== '?' && current.trim().length > 0) {
357
- if (result.length === 0) {
358
- // First segment
359
- result.push(current.trimEnd());
360
- }
361
- else {
362
- result.push(indent + current.trimEnd());
363
- }
364
- current = ch;
365
- i++;
366
- continue;
367
- }
368
- current += ch;
369
- i++;
281
+ if (ch === ')' || ch === ']' || ch === '}') {
282
+ depth--;
370
283
  }
371
- // Add remaining
372
- if (current.trim().length > 0) {
373
- if (result.length === 0) {
374
- result.push(current.trim());
375
- }
376
- else {
377
- result.push(indent + current.trim());
378
- }
284
+ if (ch === '.' && depth === 0 && i > 0 && chars[i - 1] !== '?' && current.trim().length > 0) {
285
+ if (result.length === 0) {
286
+ result.push(current.trimEnd());
287
+ } else {
288
+ result.push(indent + current.trimEnd());
289
+ }
290
+ current = ch;
291
+ i++;
292
+ continue;
379
293
  }
380
- return result.length > 1 ? result.join('\n') : source;
381
- }
382
- // ─── Batch formatting for embedded blocks ─────────────────
383
- /**
384
- * Format a block of CoffeeScript code (multiple statements).
385
- * Used for Statement bodies, CoffeeScript filters, and prologue.
386
- */
387
- export function formatCoffeeScriptBlock(source, options = {}) {
388
- const opts = { ...DEFAULT_OPTS, ...options };
389
- if (!opts.enabled || !source || source.trim() === '')
390
- return source;
391
- // For blocks, format each line individually
392
- const lines = source.split('\n');
393
- const formatted = lines.map(line => {
394
- const trimmed = line.trim();
395
- if (!trimmed)
396
- return '';
397
- // Preserve leading whitespace for indentation
398
- const leading = line.match(/^(\s*)/)?.[1] ?? '';
399
- try {
400
- const formattedExpr = formatCoffeeScript(trimmed, options);
401
- return leading + formattedExpr;
402
- }
403
- catch {
404
- return line;
405
- }
406
- });
407
- return formatted.join('\n');
408
- }
409
- //# sourceMappingURL=coffeescript-formatter.js.map
294
+ current += ch;
295
+ i++;
296
+ }
297
+ if (current.trim().length > 0) {
298
+ if (result.length === 0) {
299
+ result.push(current.trim());
300
+ } else {
301
+ result.push(indent + current.trim());
302
+ }
303
+ }
304
+ if (result.length > 1) {
305
+ return result.join('\n');
306
+ } else {
307
+ return source;
308
+ }
309
+ };