coffeehaml 0.7.6 → 0.8.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.
@@ -1,7 +1,7 @@
1
1
  // Generated by CoffeeScript 2.7.0
2
2
  // printer.coffee — AST → Prettier Doc IR
3
3
  // Core formatting engine for CoffeeHaml Prettier plugin
4
- var MergedStatements, VOID_ELEMENTS, _prettier, _require, analyzeBlankLines, defaultOpts, detectStyle, formatAttributes, formatTag, group, hardline, indent, isSingleInlineChild, isVoidElement, join, joinChildrenWithBlanks, mergeAllStatements, opts, printComment, printControlFlow, printDoc, printDoctype, printElement, printFilter, printFlow, printImplicitDiv, printMergedFlow, printNode, printOutput, reflowCommentText;
4
+ var MergedStatements, VOID_ELEMENTS, _prettier, _require, analyzeBlankLines, defaultOpts, detectStyle, formatAttributes, formatTag, group, hardline, indent, isSingleInlineChild, isVoidElement, join, joinChildrenWithBlanks, line, mergeAllStatements, opts, printCoffeeBlock, printCoffeeYield, printComment, printControlFlow, printDoc, printDoctype, printElement, printFence, printFilter, printFlow, printImplicitDiv, printMergedFlow, printNode, printOutput, reflowCommentText;
5
5
 
6
6
  import {
7
7
  Document,
@@ -12,6 +12,8 @@ import {
12
12
  ControlFlow,
13
13
  Comment,
14
14
  Filter,
15
+ CoffeeBlock,
16
+ CoffeeYield,
15
17
  Doctype,
16
18
  Node,
17
19
  Expression,
@@ -32,7 +34,7 @@ _require = createRequire(import.meta.url);
32
34
 
33
35
  _prettier = _require('prettier');
34
36
 
35
- ({group, indent, hardline, join} = _prettier.doc.builders);
37
+ ({group, indent, hardline, line, join} = _prettier.doc.builders);
36
38
 
37
39
  // ─── Format Options ────────────────────────────────────────
38
40
  // CoffeeHamlFormatOptions shape (runtime):
@@ -49,7 +51,7 @@ defaultOpts = {
49
51
  inlineThreshold: -1,
50
52
  voidElementStyle: 'self-closing',
51
53
  attributeStyle: 'preserve',
52
- attributeMultilineThreshold: 1,
54
+ attributeMultilineThreshold: 0,
53
55
  attributeSort: 'none',
54
56
  quoteStyle: 'preserve',
55
57
  coffeeScriptFormat: true,
@@ -225,6 +227,12 @@ printNode = function(path, o, printFn) {
225
227
  if (node instanceof Filter) {
226
228
  return printFilter(path, o, printFn);
227
229
  }
230
+ if (node instanceof CoffeeBlock) {
231
+ return printCoffeeBlock(path, o, printFn);
232
+ }
233
+ if (node instanceof CoffeeYield) {
234
+ return printCoffeeYield(path, o, printFn);
235
+ }
228
236
  if (node instanceof Doctype) {
229
237
  return printDoctype(path, o);
230
238
  }
@@ -259,7 +267,7 @@ printDoc = function(path, o, printFn) {
259
267
 
260
268
  // ─── Element ───────────────────────────────────────────────
261
269
  printElement = function(path, o, printFn) {
262
- var attrStyle, attrs, childDoc, childDocs, children, classes, header, id, isVoid, joined, node, ref, ref1, ref2, ref3, selfClose, tag;
270
+ var attrStyle, attrs, childDoc, childDocs, children, classes, header, id, isVoid, joined, node, ref, ref1, ref2, ref3, selfClose, sep, tag;
263
271
  node = path.node;
264
272
  tag = formatTag(node.tag, o);
265
273
  // Classes and ID
@@ -282,16 +290,18 @@ printElement = function(path, o, printFn) {
282
290
  } else if (node.isSelfClosing) {
283
291
  selfClose = '/';
284
292
  }
285
- header = `%${tag}${classes}${id}${attrs}${selfClose}`;
293
+ header = [`%${tag}${classes}${id}`, attrs, selfClose];
286
294
  // Children
287
295
  children = (ref = node.children) != null ? ref : [];
288
296
  if (children.length === 0) {
289
297
  return header;
290
298
  }
291
299
  // Inline child
292
- if (isSingleInlineChild(children) && o.controlFlowInline !== false) {
300
+ if (isSingleInlineChild(children)) {
293
301
  childDoc = (ref1 = (ref2 = path.map(printFn, 'children')) != null ? ref2[0] : void 0) != null ? ref1 : '';
294
- return `${header} ${childDoc}`;
302
+ // Output (= expr) carries its own separator; Text needs a space
303
+ sep = children[0] instanceof Output ? '' : ' ';
304
+ return [header, sep, childDoc];
295
305
  }
296
306
  childDocs = (ref3 = path.map(printFn, 'children')) != null ? ref3 : [];
297
307
  joined = joinChildrenWithBlanks(children, childDocs, o);
@@ -300,7 +310,7 @@ printElement = function(path, o, printFn) {
300
310
 
301
311
  // ─── ImplicitDiv ───────────────────────────────────────────
302
312
  printImplicitDiv = function(path, o, printFn) {
303
- var attrStyle, attrs, childDoc, childDocs, children, classes, header, id, joined, node, ref, ref1, ref2, ref3;
313
+ var attrStyle, attrs, childDoc, childDocs, children, classes, header, id, joined, node, ref, ref1, ref2, ref3, sep;
304
314
  node = path.node;
305
315
  classes = node.classes.map(function(c) {
306
316
  return `.${c}`;
@@ -308,14 +318,15 @@ printImplicitDiv = function(path, o, printFn) {
308
318
  id = node.id ? `#${node.id}` : '';
309
319
  attrStyle = detectStyle(node, o);
310
320
  attrs = formatAttributes(node, attrStyle, o);
311
- header = `${classes}${id}${attrs}`;
321
+ header = [`${classes}${id}`, attrs];
312
322
  children = (ref = node.children) != null ? ref : [];
313
323
  if (children.length === 0) {
314
324
  return header;
315
325
  }
316
326
  if (isSingleInlineChild(children)) {
317
327
  childDoc = (ref1 = (ref2 = path.map(printFn, 'children')) != null ? ref2[0] : void 0) != null ? ref1 : '';
318
- return `${header} ${childDoc}`;
328
+ sep = children[0] instanceof Output ? '' : ' ';
329
+ return [header, sep, childDoc];
319
330
  }
320
331
  childDocs = (ref3 = path.map(printFn, 'children')) != null ? ref3 : [];
321
332
  joined = joinChildrenWithBlanks(children, childDocs, o);
@@ -473,6 +484,34 @@ printFilter = function(path, o, _printFn) {
473
484
  }
474
485
  };
475
486
 
487
+ // ─── CoffeeBlock ───────────────────────────────────────────
488
+
489
+ // Build a fenced block doc using `hardline` so Prettier can re-indent the
490
+ // dedented body to the correct nesting level (raw `\n` strings would not).
491
+ printFence = function(fence, content) {
492
+ var k, len, parts, ref;
493
+ if (content) {
494
+ parts = [fence];
495
+ ref = content.split('\n');
496
+ for (k = 0, len = ref.length; k < len; k++) {
497
+ line = ref[k];
498
+ parts.push(hardline, line);
499
+ }
500
+ parts.push(hardline, fence);
501
+ return parts;
502
+ } else {
503
+ return [fence, hardline, fence];
504
+ }
505
+ };
506
+
507
+ printCoffeeBlock = function(path, o, _printFn) {
508
+ return printFence('---', path.node.content);
509
+ };
510
+
511
+ printCoffeeYield = function(path, o, _printFn) {
512
+ return printFence('===', path.node.content);
513
+ };
514
+
476
515
  // ─── Doctype ───────────────────────────────────────────────
477
516
  printDoctype = function(path, o) {
478
517
  var node;
@@ -501,12 +540,14 @@ detectStyle = function(node, o) {
501
540
  };
502
541
 
503
542
  formatAttributes = function(node, style, o) {
504
- var attr, attrs, bareParts, expr, k, len, name, parts, ref, ref1, ref2, val;
543
+ var attr, attrs, bareParts, close, expr, forceBreak, k, len, name, open, parts, ref, ref1, ref2, sep, val;
505
544
  attrs = (ref = node.attributes) != null ? ref : [];
506
545
  if (!(attrs.length > 0)) {
507
546
  return '';
508
547
  }
509
- // Bare style: HTML-style key="val" key2={expr} — space-separated
548
+ // Bare style: HTML-style key="val" key2={expr} — space-separated.
549
+ // Never folded: an indented continuation line would parse as a child
550
+ // element, silently changing the tree. Bare attributes stay inline.
510
551
  if (style === 'bare') {
511
552
  bareParts = (function() {
512
553
  var k, len, ref1, ref2, results;
@@ -528,38 +569,44 @@ formatAttributes = function(node, style, o) {
528
569
  return results;
529
570
  })();
530
571
  return ` ${bareParts.join(' ')}`;
531
- } else {
532
- parts = [];
533
- for (k = 0, len = attrs.length; k < len; k++) {
534
- attr = attrs[k];
535
- if (attr.spread) {
536
- expr = (ref1 = attr.expression.source) != null ? ref1 : '';
537
- if (o.coffeeScriptFormat) {
538
- expr = formatCoffeeScript(expr, o);
539
- }
540
- parts.push(style === 'parens' ? `...${expr}` : `{${expr}...}`);
541
- continue;
542
- }
543
- name = attr.name;
544
- val = (ref2 = attr.value.source) != null ? ref2 : '';
572
+ }
573
+ parts = [];
574
+ for (k = 0, len = attrs.length; k < len; k++) {
575
+ attr = attrs[k];
576
+ if (attr.spread) {
577
+ expr = (ref1 = attr.expression.source) != null ? ref1 : '';
545
578
  if (o.coffeeScriptFormat) {
546
- val = formatCoffeeScript(val, o);
547
- }
548
- if (attr.shorthand) {
549
- parts.push(name);
550
- } else {
551
- parts.push(`${name}: ${val}`);
579
+ expr = formatCoffeeScript(expr, o);
552
580
  }
581
+ parts.push(style === 'parens' ? `...${expr}` : `{${expr}...}`);
582
+ continue;
553
583
  }
554
- switch (style) {
555
- case 'braces':
556
- return `{ ${parts.join(', ')} }`;
557
- case 'parens':
558
- return `( ${parts.join(', ')} )`;
559
- default:
560
- return parts.join(', ');
584
+ name = attr.name;
585
+ val = (ref2 = attr.value.source) != null ? ref2 : '';
586
+ if (o.coffeeScriptFormat) {
587
+ val = formatCoffeeScript(val, o);
588
+ }
589
+ if (attr.shorthand) {
590
+ parts.push(name);
591
+ } else {
592
+ parts.push(`${name}: ${val}`);
561
593
  }
562
594
  }
595
+ // Count-based forcing: a positive threshold breaks even when the
596
+ // attributes would otherwise fit within printWidth.
597
+ forceBreak = o.attributeMultilineThreshold > 0 && attrs.length >= o.attributeMultilineThreshold;
598
+ sep = forceBreak ? hardline : line;
599
+ if (style === 'braces') {
600
+ open = '{';
601
+ close = '}';
602
+ } else if (style === 'parens') {
603
+ open = '(';
604
+ close = ')';
605
+ } else {
606
+ // Unknown style — inline fallback, no folding.
607
+ return parts.join(', ');
608
+ }
609
+ return group([open, indent([sep, join([',', sep], parts)]), sep, close]);
563
610
  };
564
611
 
565
612
  // ─── CoffeeScript Embed (for Prettier embed) ───────────────
package/docs/grammar.md CHANGED
@@ -292,6 +292,72 @@ depending on configuration.
292
292
 
293
293
  ---
294
294
 
295
+ ## CoffeeScript Blocks (`---`)
296
+
297
+ ```
298
+ CoffeeBlock := '---' Newline
299
+ CoffeeBody
300
+ '---' Newline
301
+
302
+ CoffeeBody := Raw CoffeeScript lines, dedented to column 0.
303
+ ```
304
+
305
+ A fenced raw CoffeeScript area. The body between the two `---` lines is
306
+ taken verbatim (dedented by its minimum indentation) and compiled to
307
+ executable JavaScript via the CoffeeScript bridge, then hoisted to module
308
+ scope — outside any component wrapper. This is how imports, helper
309
+ functions and one-time setup code are expressed without fighting HAML's
310
+ indentation tree.
311
+
312
+ ```haml
313
+ ---
314
+ import { blah } from 'ok'
315
+ answer = 42
316
+ ---
317
+ %p= answer
318
+ ```
319
+
320
+ Semantics:
321
+
322
+ - **Escapes indentation**: body lines are dedented to column 0, so a block
323
+ nested under an element still writes CoffeeScript flush-left.
324
+ - **Module scope**: the compiled statements run once at module load, before
325
+ any JSX. `import`/`export` therefore stay valid (they are not wrapped in
326
+ a component function).
327
+ - **No markup**: the block contributes nothing to the render tree.
328
+
329
+ Note: `---` is a CoffeeHaml fence marker, not an element. For a horizontal
330
+ rule use `%hr` — mapping `---` to `<hr>` is a Markdown/YAML convention, not
331
+ Haml.
332
+
333
+ ---
334
+
335
+ ## Yield Blocks (`===`)
336
+
337
+ ```
338
+ CoffeeYield := '===' Newline
339
+ CoffeeBody
340
+ '===' Newline
341
+ ```
342
+
343
+ The `===` fence is the multiline counterpart to `=` (output): its body is
344
+ CoffeeScript, dedented to column 0 exactly like a `---` block, but instead
345
+ of being hoisted to module scope its **final value is yielded as content**
346
+ at that position in the render tree (escaped, matching `=`).
347
+
348
+ ```haml
349
+ %p
350
+ ===
351
+ name = computeName()
352
+ "Hello, #{name}"
353
+ ===
354
+ ```
355
+
356
+ A single-expression body compiles directly; a multi-statement body is
357
+ wrapped in an IIFE so the last expression is the yielded value.
358
+
359
+ ---
360
+
295
361
  ## Raw Text / Passthrough
296
362
 
297
363
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coffeehaml",
3
- "version": "0.7.6",
3
+ "version": "0.8.0",
4
4
  "description": "Haml structure. CoffeeScript semantics. React runtime. Zero runtime. Vite-native.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -62,7 +62,7 @@
62
62
  "scripts": {
63
63
  "build": "coffee -c -o dist/ src/*.coffee && coffee -c -o dist/prettier/ src/prettier/*.coffee",
64
64
  "dev": "coffee -c -w -o dist/ src/*.coffee & coffee -c -w -o dist/prettier/ src/prettier/*.coffee",
65
- "test": "node test/prettier/run-integration-tests.mjs && node test/prettier/run-coffeescript-tests.mjs && node test/prettier/run-statement-merge-tests.mjs",
65
+ "test": "node test/run-compiler-tests.mjs && node test/run-coffee-block-tests.mjs && node test/prettier/run-integration-tests.mjs && node test/prettier/run-coffeescript-tests.mjs && node test/prettier/run-statement-merge-tests.mjs",
66
66
  "prepublishOnly": "npm run build",
67
67
  "fmt": "prettier --write --plugin=. **/*.chaml"
68
68
  },
@@ -91,4 +91,4 @@
91
91
  "dependencies": {
92
92
  "source-map": "^0.7.4"
93
93
  }
94
- }
94
+ }
@@ -1,309 +0,0 @@
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
-
22
- // ─── Spacing categories ───────────────────────────────────
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
41
- };
42
-
43
- // ─── Public API ───────────────────────────────────────────
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 : '';
103
- try {
104
- formattedExpr = formatCoffeeScript(trimmed, options);
105
- return leading + formattedExpr;
106
- } catch (error) {
107
- return line;
108
- }
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;
120
- }
121
- if (t.generated) {
122
- return true;
123
- }
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;
165
- prevVisible = cur;
166
- }
167
- continue;
168
- }
169
- if (cur.type === 'OUTDENT') {
170
- continue;
171
- }
172
- // Skip TERMINATOR
173
- if (cur.type === 'TERMINATOR') {
174
- continue;
175
- }
176
- // Space before this token
177
- if (prevVisible && ((ref2 = prevVisible.type) !== 'INDENT' && ref2 !== 'OUTDENT')) {
178
- if (needsSpaceBetween(prevVisible, cur, prevWasAtExprStart)) {
179
- parts.push(' ');
180
- }
181
- }
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)) {
209
- return false;
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 !== '-') {
245
- return false;
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++;
280
- }
281
- if (ch === ')' || ch === ']' || ch === '}') {
282
- depth--;
283
- }
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;
293
- }
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
- };