coffeehaml 0.6.0 → 0.7.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 (66) hide show
  1. package/README.md +21 -5
  2. package/dist/ast.js +334 -123
  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 -4
  9. package/dist/lexer.js +536 -340
  10. package/dist/parser.js +533 -432
  11. package/dist/prettier/coffeescript-formatter.js +309 -0
  12. package/dist/prettier/index.js +105 -0
  13. package/dist/prettier/options.js +232 -0
  14. package/dist/prettier/printer.js +559 -0
  15. package/dist/src/ast.js +338 -0
  16. package/dist/src/cli.js +430 -0
  17. package/dist/src/compiler.js +132 -0
  18. package/dist/src/emitter.js +791 -0
  19. package/dist/src/errors.js +5 -0
  20. package/dist/src/expressions.js +223 -0
  21. package/dist/src/index.js +22 -0
  22. package/dist/src/lexer.js +504 -0
  23. package/dist/src/parser.js +575 -0
  24. package/dist/src/types.js +122 -0
  25. package/dist/src/vite-plugin.js +78 -0
  26. package/dist/types.js +122 -12
  27. package/dist/vite-plugin.js +78 -63
  28. package/docs/examples.md +201 -1
  29. package/docs/grammar.md +90 -1
  30. package/docs/index.html +260 -37
  31. package/package.json +12 -10
  32. package/syntaxes/CoffeeHaml.tmLanguage +815 -868
  33. package/syntaxes/coffeehaml.tmLanguage.json +352 -161
  34. package/dist/ast.d.ts +0 -117
  35. package/dist/ast.d.ts.map +0 -1
  36. package/dist/ast.js.map +0 -1
  37. package/dist/cli.d.ts +0 -3
  38. package/dist/cli.d.ts.map +0 -1
  39. package/dist/cli.js.map +0 -1
  40. package/dist/compiler.d.ts +0 -10
  41. package/dist/compiler.d.ts.map +0 -1
  42. package/dist/compiler.js.map +0 -1
  43. package/dist/emitter.d.ts +0 -4
  44. package/dist/emitter.d.ts.map +0 -1
  45. package/dist/emitter.js.map +0 -1
  46. package/dist/errors.d.ts +0 -3
  47. package/dist/errors.d.ts.map +0 -1
  48. package/dist/errors.js.map +0 -1
  49. package/dist/expressions.d.ts +0 -22
  50. package/dist/expressions.d.ts.map +0 -1
  51. package/dist/expressions.js.map +0 -1
  52. package/dist/index.d.ts +0 -5
  53. package/dist/index.d.ts.map +0 -1
  54. package/dist/index.js.map +0 -1
  55. package/dist/lexer.d.ts +0 -30
  56. package/dist/lexer.d.ts.map +0 -1
  57. package/dist/lexer.js.map +0 -1
  58. package/dist/parser.d.ts +0 -10
  59. package/dist/parser.d.ts.map +0 -1
  60. package/dist/parser.js.map +0 -1
  61. package/dist/types.d.ts +0 -76
  62. package/dist/types.d.ts.map +0 -1
  63. package/dist/types.js.map +0 -1
  64. package/dist/vite-plugin.d.ts +0 -7
  65. package/dist/vite-plugin.d.ts.map +0 -1
  66. package/dist/vite-plugin.js.map +0 -1
@@ -0,0 +1,78 @@
1
+ // Generated by CoffeeScript 2.7.0
2
+ // vite-plugin.coffee — Vite plugin for CoffeeHaml
3
+ var COFFEEHAML_RE, coffeehaml, componentNameFromPath;
4
+
5
+ import {
6
+ compile
7
+ } from './compiler.js';
8
+
9
+ import {
10
+ basename
11
+ } from 'path';
12
+
13
+ COFFEEHAML_RE = /\.(coffeehaml|cohaml|chaml)$/;
14
+
15
+ componentNameFromPath = function(filepath) {
16
+ var stem;
17
+ stem = basename(filepath).split('.').shift() || 'Component';
18
+ return stem.charAt(0).toUpperCase() + stem.slice(1);
19
+ };
20
+
21
+ export default coffeehaml = function(options = {}) {
22
+ var compilerOpts, ref;
23
+ compilerOpts = Object.assign({
24
+ sourceMap: true,
25
+ wrap: 'component'
26
+ }, (ref = options.compilerOptions) != null ? ref : {});
27
+ return {
28
+ name: 'coffeehaml',
29
+ transform: function(code, id) {
30
+ var err, finalCode, name, postamble, preamble, result;
31
+ if (!COFFEEHAML_RE.test(id)) {
32
+ return;
33
+ }
34
+ result = compile(code, ...compilerOpts, {
35
+ filename: id,
36
+ componentName: compilerOpts.componentName || componentNameFromPath(id)
37
+ });
38
+ preamble = '';
39
+ postamble = '';
40
+ if (compilerOpts.wrap && compilerOpts.wrap !== 'none' && !Array.isArray(compilerOpts.wrap)) {
41
+ preamble = '// @refresh reset\n';
42
+ name = compilerOpts.componentName || componentNameFromPath(id);
43
+ postamble = `\nif (import.meta.hot) {
44
+ import.meta.hot.accept((mod) => {
45
+ if (mod?.${name}) import.meta.hot?.data?.refresh?.();
46
+ });
47
+ }\n`;
48
+ }
49
+ finalCode = preamble + result.code + postamble;
50
+ if (result.errors.length > 0) {
51
+ err = result.errors[0];
52
+ this.error({
53
+ message: err.message
54
+ }, id, {
55
+ loc: err.location ? {
56
+ line: err.location.start.line + 1,
57
+ column: err.location.start.column + 1
58
+ } : void 0
59
+ });
60
+ return null;
61
+ }
62
+ return {
63
+ code: finalCode,
64
+ map: result.sourceMap ? {
65
+ mappings: result.sourceMap
66
+ } : null
67
+ };
68
+ },
69
+ handleHotUpdate: function({file, server}) {
70
+ if (COFFEEHAML_RE.test(file)) {
71
+ server.ws.send({
72
+ type: 'full-reload'
73
+ });
74
+ return [];
75
+ }
76
+ }
77
+ };
78
+ };
package/dist/types.js CHANGED
@@ -1,12 +1,122 @@
1
- /** Compiler error with source location. */
2
- export class CompileError extends Error {
3
- constructor(message, phase, code, location, hint) {
4
- super(message);
5
- this.name = 'CompileError';
6
- this.phase = phase;
7
- this.code = code;
8
- this.location = location;
9
- this.hint = hint;
10
- }
11
- }
12
- //# sourceMappingURL=types.js.map
1
+ // Generated by CoffeeScript 2.7.0
2
+ // types.coffee — shared types for CoffeeHaml compiler
3
+ // CoffeeScript version — interfaces become plain classes with JSDoc
4
+
5
+ // ─── Source Location ───────────────────────────────────────
6
+ export var SourcePosition = (function() {
7
+ class SourcePosition {
8
+ constructor(line, column) {
9
+ this.line = line;
10
+ this.column = column;
11
+ }
12
+
13
+ };
14
+
15
+ SourcePosition.prototype.line = 0; // 0-based
16
+
17
+ SourcePosition.prototype.column = 0; // 0-based
18
+
19
+ return SourcePosition;
20
+
21
+ }).call(this);
22
+
23
+ export var SourceLocation = (function() {
24
+ class SourceLocation {
25
+ constructor({start, end, offset, length, file}) {
26
+ this.start = start;
27
+ this.end = end;
28
+ this.offset = offset;
29
+ this.length = length;
30
+ this.file = file;
31
+ }
32
+
33
+ };
34
+
35
+ SourceLocation.prototype.start = null; // SourcePosition
36
+
37
+ SourceLocation.prototype.end = null; // SourcePosition
38
+
39
+ SourceLocation.prototype.offset = 0;
40
+
41
+ SourceLocation.prototype.length = 0;
42
+
43
+ SourceLocation.prototype.file = null;
44
+
45
+ return SourceLocation;
46
+
47
+ }).call(this);
48
+
49
+ export var CompileError = (function() {
50
+ // ─── Compile Error ─────────────────────────────────────────
51
+ class CompileError extends Error {
52
+ constructor(message, phase, code, location = null, hint = null) {
53
+ super(message);
54
+ this.phase = phase;
55
+ this.code = code;
56
+ this.location = location;
57
+ this.hint = hint;
58
+ this.name = 'CompileError';
59
+ }
60
+
61
+ };
62
+
63
+ CompileError.prototype.location = null;
64
+
65
+ CompileError.prototype.phase = '';
66
+
67
+ CompileError.prototype.hint = null;
68
+
69
+ CompileError.prototype.code = '';
70
+
71
+ return CompileError;
72
+
73
+ }).call(this);
74
+
75
+ export var CompileWarning = (function() {
76
+ // ─── Compile Warning ───────────────────────────────────────
77
+ class CompileWarning {
78
+ constructor(message1, location = null, phase = '') {
79
+ this.message = message1;
80
+ this.location = location;
81
+ this.phase = phase;
82
+ }
83
+
84
+ };
85
+
86
+ CompileWarning.prototype.message = '';
87
+
88
+ CompileWarning.prototype.location = null;
89
+
90
+ CompileWarning.prototype.phase = '';
91
+
92
+ return CompileWarning;
93
+
94
+ }).call(this);
95
+
96
+ // ─── Compiler Options ──────────────────────────────────────
97
+ export var CompilerOptions = null; // plain object, documented below
98
+
99
+
100
+ // CompilerOptions shape:
101
+ // filename?: string
102
+ // sourceMap?: boolean
103
+ // jsxRuntime?: string
104
+ // jsxImportSource?: string
105
+ // filters?: { [name: string]: (content: string, filterName: string) => string }
106
+ // wrap?: 'none' | 'component' | 'observer' | string[]
107
+ // componentName?: string
108
+
109
+ // ─── Compile Result ────────────────────────────────────────
110
+ export var CompileResult = null; // { code, sourceMap?, errors: CompileError[], warnings: CompileWarning[] }
111
+
112
+
113
+ // ─── Emitter Options ───────────────────────────────────────
114
+ export var EmitterOptions = null; // same shape as CompilerOptions without export-related fields
115
+
116
+
117
+ // ─── Emit Result ───────────────────────────────────────────
118
+ export var EmitResult = null; // { code, sourceMap?, warnings: CompileWarning[] }
119
+
120
+
121
+ // ─── Filter Handler ────────────────────────────────────────
122
+ export var FilterHandler = null; // (content: string, filterName: string) => string
@@ -1,63 +1,78 @@
1
- import { compile } from './compiler.js';
2
- import { basename } from 'path';
3
- const COFFEEHAML_RE = /\.(coffeehaml|cohaml|chaml)$/;
4
- /** Derive a PascalCase component name from a filename stem. */
5
- function componentNameFromPath(filepath) {
6
- const stem = basename(filepath).split('.').shift() || 'Component';
7
- return stem.charAt(0).toUpperCase() + stem.slice(1);
8
- }
9
- export default function coffeehaml(options = {}) {
10
- // Default to component mode — the only sensible default for Vite/React projects
11
- const compilerOpts = {
12
- sourceMap: true,
13
- wrap: 'component',
14
- ...options.compilerOptions,
15
- };
16
- return {
17
- name: 'coffeehaml',
18
- transform(code, id) {
19
- if (!COFFEEHAML_RE.test(id))
20
- return;
21
- const result = compile(code, {
22
- ...compilerOpts,
23
- filename: id,
24
- componentName: compilerOpts.componentName || componentNameFromPath(id),
25
- });
26
- // Inject React Fast Refresh pragma and HMR accept when in component mode
27
- let preamble = '';
28
- let postamble = '';
29
- if (compilerOpts.wrap && compilerOpts.wrap !== 'none' && !compilerOpts.wrap.includes) {
30
- // Component or observer mode — add Fast Refresh support
31
- preamble = '// @refresh reset\n';
32
- const name = compilerOpts.componentName || componentNameFromPath(id);
33
- postamble = `\nif (import.meta.hot) {\n import.meta.hot.accept((mod) => {\n if (mod?.${name}) import.meta.hot?.data?.refresh?.();\n });\n}\n`;
34
- }
35
- const finalCode = preamble + result.code + postamble;
36
- if (result.errors.length > 0) {
37
- const err = result.errors[0];
38
- this.error({
39
- message: err.message,
40
- id,
41
- // @ts-ignore
42
- loc: err.location ? {
43
- line: err.location.start.line + 1,
44
- column: err.location.start.column + 1,
45
- } : undefined,
46
- });
47
- return null;
48
- }
49
- return {
50
- code: finalCode,
51
- map: result.sourceMap ? { mappings: result.sourceMap } : null,
52
- };
53
- },
54
- handleHotUpdate({ file, server }) {
55
- if (COFFEEHAML_RE.test(file)) {
56
- // Invalidate and reload — the transform hook will recompile
57
- server.ws.send({ type: 'full-reload' });
58
- return [];
59
- }
60
- },
61
- };
62
- }
63
- //# sourceMappingURL=vite-plugin.js.map
1
+ // Generated by CoffeeScript 2.7.0
2
+ // vite-plugin.coffee — Vite plugin for CoffeeHaml
3
+ var COFFEEHAML_RE, coffeehaml, componentNameFromPath;
4
+
5
+ import {
6
+ compile
7
+ } from './compiler.js';
8
+
9
+ import {
10
+ basename
11
+ } from 'path';
12
+
13
+ COFFEEHAML_RE = /\.(coffeehaml|cohaml|chaml)$/;
14
+
15
+ componentNameFromPath = function(filepath) {
16
+ var stem;
17
+ stem = basename(filepath).split('.').shift() || 'Component';
18
+ return stem.charAt(0).toUpperCase() + stem.slice(1);
19
+ };
20
+
21
+ export default coffeehaml = function(options = {}) {
22
+ var compilerOpts, ref;
23
+ compilerOpts = Object.assign({
24
+ sourceMap: true,
25
+ wrap: 'component'
26
+ }, (ref = options.compilerOptions) != null ? ref : {});
27
+ return {
28
+ name: 'coffeehaml',
29
+ transform: function(code, id) {
30
+ var err, finalCode, name, postamble, preamble, result;
31
+ if (!COFFEEHAML_RE.test(id)) {
32
+ return;
33
+ }
34
+ result = compile(code, ...compilerOpts, {
35
+ filename: id,
36
+ componentName: compilerOpts.componentName || componentNameFromPath(id)
37
+ });
38
+ preamble = '';
39
+ postamble = '';
40
+ if (compilerOpts.wrap && compilerOpts.wrap !== 'none' && !Array.isArray(compilerOpts.wrap)) {
41
+ preamble = '// @refresh reset\n';
42
+ name = compilerOpts.componentName || componentNameFromPath(id);
43
+ postamble = `\nif (import.meta.hot) {
44
+ import.meta.hot.accept((mod) => {
45
+ if (mod?.${name}) import.meta.hot?.data?.refresh?.();
46
+ });
47
+ }\n`;
48
+ }
49
+ finalCode = preamble + result.code + postamble;
50
+ if (result.errors.length > 0) {
51
+ err = result.errors[0];
52
+ this.error({
53
+ message: err.message
54
+ }, id, {
55
+ loc: err.location ? {
56
+ line: err.location.start.line + 1,
57
+ column: err.location.start.column + 1
58
+ } : void 0
59
+ });
60
+ return null;
61
+ }
62
+ return {
63
+ code: finalCode,
64
+ map: result.sourceMap ? {
65
+ mappings: result.sourceMap
66
+ } : null
67
+ };
68
+ },
69
+ handleHotUpdate: function({file, server}) {
70
+ if (COFFEEHAML_RE.test(file)) {
71
+ server.ws.send({
72
+ type: 'full-reload'
73
+ });
74
+ return [];
75
+ }
76
+ }
77
+ };
78
+ };
package/docs/examples.md CHANGED
@@ -371,6 +371,206 @@ Haml comments and their nested children are stripped entirely.
371
371
  init = ->
372
372
  console.log "ready"
373
373
  init()
374
+
375
+ ---
376
+
377
+ ## 11. Statement Continuation (v0.4.2)
378
+
379
+ Indented children of `-` control blocks compile as continuation code lines.
380
+ No more single-line straitjacket for complex logic:
381
+
382
+ ```haml
383
+ - if user.role == 'admin' && user.permissions.includes('write')
384
+ %AdminPanel
385
+ %h2 Admin
386
+ %p Welcome back, = user.name
387
+ ```
388
+
389
+ Multi-statement blocks:
390
+
391
+ ```haml
392
+ - if results.length > 0
393
+ sorted = results.sort (a, b) -> b.score - a.score
394
+ topThree = sorted.slice 0, 3
395
+ %Results{ data: topThree }
396
+ ```
397
+
398
+ Works with `for`, `while`, and bare statements too:
399
+
400
+ ```haml
401
+ %div
402
+ - x = computeBaseValue()
403
+ - x *= multiplier
404
+ - x += offset
405
+ %p= x
406
+ ```
407
+
408
+ ---
409
+
410
+ ## 12. Expression Continuation (v0.5.0)
411
+
412
+ Long inline expressions can break across indented continuation lines:
413
+
414
+ ```haml
415
+ %h1= "Welcome, " +
416
+ user.firstName + " " +
417
+ user.lastName
418
+
419
+ %div= someFunction(
420
+ arg1,
421
+ arg2,
422
+ arg3
423
+ )
424
+ ```
425
+
426
+ Indented TEXT children of `=` are joined into the expression before compilation:
427
+
428
+ ```haml
429
+ %p= formatMessage(
430
+ user.name,
431
+ user.role,
432
+ if premium then "⭐" else ""
433
+ )
434
+ ```
435
+
436
+ ---
437
+
438
+ ## 13. Arrow Continuation (v0.6.0)
439
+
440
+ `=` with an arrow function can have indented element children — the elements
441
+ become the arrow body's JSX:
442
+
443
+ ```haml
444
+ %ul
445
+ = items.map (item) ->
446
+ %li{ key: item.id }
447
+ %span.icon= item.icon
448
+ %a{ href: item.url }= item.label
449
+ ```
450
+
451
+ Compiles to:
452
+
453
+ ```js
454
+ jsx("ul", null,
455
+ ...items.map(item => jsx("li", { key: item.id },
456
+ jsx("span", { className: "icon" }, item.icon),
457
+ jsx("a", { href: item.url }, item.label)
458
+ ))
459
+ )
460
+ ```
461
+
462
+ Works inline within elements too:
463
+
464
+ ```haml
465
+ %Dashboard
466
+ = widgets.filter (w) -> w.active
467
+ .map (w) ->
468
+ %WidgetCard{ widget: w, key: w.id }
469
+ ```
470
+
471
+ ---
472
+
473
+ ## 14. CLI Init Scaffold (v0.6.0)
474
+
475
+ Interactive project setup wizard:
476
+
477
+ ```bash
478
+ $ npx coffeehaml init
479
+
480
+ ☕ CoffeeHaml init — project scaffold
481
+ Working directory: /my-react-app
482
+ Detected: package.json, Vite, TypeScript
483
+
484
+ Add coffeehaml as devDependency? (Y/n):
485
+ Configure Vite plugin? (Y/n):
486
+ Create sample .chaml component? (Y/n):
487
+ Add build/watch scripts to package.json? (Y/n):
488
+ ```
489
+
490
+ Detects your project type, installs dependencies, patches Vite config,
491
+ creates a sample component, and adds npm scripts — all interactive.
492
+
493
+ ---
494
+
495
+ ## 15. Watch Mode (v0.5.0)
496
+
497
+ ```bash
498
+ # Watch and recompile on every change
499
+ $ npx coffeehaml watch src/ --wrap component
500
+
501
+ # With observer HOC wrapping
502
+ $ npx coffeehaml watch src/ --wrap observer
503
+ ```
504
+
505
+ Uses native `fs.watch` — zero dependencies. Recursively scans on startup,
506
+ recompiles individual files on change.
507
+
508
+ ---
509
+
510
+ ## 16. HOC Wrapping (v0.4.0)
511
+
512
+ Wrap components in higher-order functions:
513
+
514
+ ```haml
515
+ -# With Vite plugin (default: component)
516
+ -# Or via CLI / Node API:
517
+
518
+ %div.page
519
+ %h1 Hello
520
+ ```
521
+
522
+ ```bash
523
+ # Single HOC
524
+ $ npx coffeehaml compile app.chaml --wrap observer
525
+ # → export default observer(function App(props) { ... })
526
+
527
+ # HOC chain
528
+ $ npx coffeehaml compile app.chaml --wrap observer,memo,forwardRef
529
+ # → export default observer(memo(forwardRef(function App(props) { ... })))
530
+ ```
531
+
532
+ Node API:
533
+
534
+ ```js
535
+ compile(source, { wrap: ['observer', 'memo'] })
536
+ compile(source, { wrap: 'observer' })
537
+ compile(source, { wrap: 'component' }) // plain component
538
+ ```
539
+
540
+ ---
541
+
542
+ ## 17. Multi-Error Parser Recovery (v0.6.0)
543
+
544
+ The parser no longer stops at the first error. It collects all errors,
545
+ recovers at structural boundaries, and returns a partial AST:
546
+
547
+ ```js
548
+ const result = compile(brokenSource);
549
+ console.log(result.errors);
550
+ // [
551
+ // { message: "Unexpected token '}'", location: { line: 3, column: 12 } },
552
+ // { message: "Unclosed attribute block", location: { line: 7, column: 1 } },
553
+ // ]
554
+ ```
555
+
556
+ Errors carry source locations (line/column) for IDE integration and
557
+ clickable terminal output.
558
+
559
+ ---
560
+
561
+ ## 18. Source-Located Errors (v0.5.0)
562
+
563
+ All errors carry file, line, and column:
564
+
565
+ ```bash
566
+ $ npx coffeehaml compile bad.chaml
567
+ Error: bad.chaml:12:3 — Unexpected ')' in expression
568
+ %div{ onClick: -> handle() ) }
569
+ ^
570
+ ```
571
+
572
+ CoffeeScript compile errors are wrapped with source location context,
573
+ making it clear which `.chaml` line triggered the failure.
374
574
  ```
375
575
 
376
576
  ### Markdown (build-time)
@@ -716,4 +916,4 @@ Or use a raw CoffeeScript expression:
716
916
  ```
717
917
 
718
918
  Fragments are implicit — multiple top-level nodes in a Document emit
719
- a React Fragment automatically.
919
+ a React Fragment automatically.
package/docs/grammar.md CHANGED
@@ -341,6 +341,95 @@ arr...
341
341
  "Hello, #{name}"
342
342
 
343
343
  // Heregex
344
+ ```
345
+
346
+ ---
347
+
348
+ ## Continuation (v0.4.2 / v0.5.0 / v0.6.0)
349
+
350
+ CoffeeHaml supports three forms of indented continuation, extending the
351
+ grammar beyond single-line constructs.
352
+
353
+ ### Statement Continuation (`-`, v0.4.2)
354
+
355
+ ```
356
+ Statement := '-' Whitespace CodeStatement Newline
357
+ (Indent CodeLine+ Dedent)?
358
+
359
+ CodeLine := [^\\n]+ Newline
360
+ // Indented code lines concatenated to the primary statement.
361
+ // Enables multi-line CoffeeScript inside - blocks.
362
+ ```
363
+
364
+ The indented lines are joined to the primary statement with newline
365
+ separators before CoffeeScript compilation:
366
+
367
+ ```haml
368
+ - x = computeValue()
369
+ x *= multiplier # continuation
370
+ x += offset # continuation
371
+ %p= x
372
+ ```
373
+
374
+ Compiles as a single CoffeeScript block: `x = computeValue(); x *= multiplier; x += offset`.
375
+
376
+ ### Expression Continuation (`=`, v0.5.0)
377
+
378
+ ```
379
+ Output := '=' Whitespace Expression Newline
380
+ (Indent TextLine+ Dedent)?
381
+
382
+ TextLine := [^\\n]+ Newline
383
+ // TEXT children of = outputs are joined to the expression.
384
+ ```
385
+
386
+ Indented TEXT children of `=` outputs are concatenated into the expression:
387
+
388
+ ```haml
389
+ %h1= "Hello, " +
390
+ user.firstName +
391
+ " " +
392
+ user.lastName
393
+ ```
394
+
395
+ ### Arrow Continuation (`= expr ->`, v0.6.0)
396
+
397
+ ```
398
+ ArrowOutput := '=' Whitespace Expression ArrowSuffix Newline
399
+ (Indent Node+ Dedent)?
400
+
401
+ ArrowSuffix := '->' | '=>' Arguments?
402
+ // Detected after CoffeeScript expression parsing.
403
+ // Triggers element-body mode for indented children.
404
+ ```
405
+
406
+ When `=` is followed by an expression ending in a CoffeeScript arrow
407
+ (`->` or `=>`), the indented children become the arrow body — compiled
408
+ as JSX elements within the arrow function:
409
+
410
+ ```haml
411
+ = items.map (item) ->
412
+ %li{ key: item.id }
413
+ %span= item.label
414
+ ```
415
+
416
+ Compiles to:
417
+
418
+ ```js
419
+ items.map(item => jsx("li", { key: item.id },
420
+ jsx("span", null, item.label)
421
+ ))
422
+ ```
423
+
424
+ This works both at the module level (`emitOutput`) and inline within
425
+ elements (`emitChildToJs`), enabling functional mapping with full element
426
+ syntax in both positions.
427
+
428
+ ### Continuation Precedence
429
+
430
+ 1. Arrow continuation takes priority — if the expression ends with `->` or `=>`, children are treated as the arrow body.
431
+ 2. Otherwise, TEXT children are joined as expression continuations (for multi-line expressions).
432
+ 3. Statement continuations are always code lines, joined before CoffeeScript compilation.
344
433
  /// pattern /g
345
434
 
346
435
  // Destructuring
@@ -429,4 +518,4 @@ it checks patterns in this order:
429
518
  7. `-` → ControlFlow
430
519
  8. `=` / `==` → Output
431
520
  9. `:` + identifier → Filter
432
- 10. anything else → RawText
521
+ 10. anything else → RawText