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
package/dist/emitter.js CHANGED
@@ -1,756 +1,791 @@
1
- import { Element, ImplicitDiv, Text, Output, ControlFlow, Comment, Filter, Doctype, Expression, } from './ast.js';
2
- import { compileExpression, compileStatement } from './expressions.js';
3
- import { SourceMapGenerator } from 'source-map';
4
- // ─── Void elements (self-closing, no children) ─────────────
5
- const VOID_ELEMENTS = new Set([
6
- 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
7
- 'link', 'meta', 'param', 'source', 'track', 'wbr',
8
- ]);
9
- // ─── Emit State ────────────────────────────────────────────
10
- class EmitState {
11
- constructor(options = {}) {
12
- this.indent = 0;
13
- this.output = '';
14
- this.needsFragment = false;
15
- this.needsImport = false;
16
- this.sourceMapGenerator = null;
17
- this.outputLine = 0;
18
- this.warnings = [];
19
- this.hoisted = []; // statement JS hoisted before return
20
- this.directEmit = false; // emit statements inline (wrap='none')
21
- this.options = options;
22
- if (options.sourceMap && options.filename) {
23
- this.sourceMapGenerator = new SourceMapGenerator({ file: options.filename });
24
- }
1
+ // Generated by CoffeeScript 2.7.0
2
+ // emitter.coffee — AST → JSX/JS code generation
3
+ var EmitState, VOID_ELEMENTS, attrNameToJs, buildAttributes, compileComponentBody, emitArrowOutput, emitArrowOutputToJs, emitChildToJs, emitComment, emitConditional, emitControlFlow, emitControlFlowToJs, emitDoctype, emitElement, emitElseBranch, emitFilter, emitFilterToJs, emitImplicitDiv, emitLoop, emitNode, emitNodes, emitOutput, emitStatement, emitText, escapeString, mapSourceLocation, splitJsStatements, tagToJs;
4
+
5
+ import {
6
+ Document,
7
+ Element,
8
+ ImplicitDiv,
9
+ Text,
10
+ Output,
11
+ ControlFlow,
12
+ Comment,
13
+ Filter,
14
+ Doctype,
15
+ Node,
16
+ Expression
17
+ } from './ast.js';
18
+
19
+ import {
20
+ compileExpression,
21
+ compileStatement
22
+ } from './expressions.js';
23
+
24
+ import {
25
+ CompileWarning
26
+ } from './types.js';
27
+
28
+ import {
29
+ SourceMapGenerator
30
+ } from 'source-map';
31
+
32
+ VOID_ELEMENTS = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
33
+
34
+ EmitState = (function() {
35
+ // ─── Emit State ────────────────────────────────────────────
36
+ class EmitState {
37
+ constructor(options1 = {}) {
38
+ this.options = options1;
39
+ if (this.options.sourceMap && this.options.filename) {
40
+ this.sourceMapGenerator = new SourceMapGenerator({
41
+ file: this.options.filename
42
+ });
43
+ }
25
44
  }
45
+
26
46
  emit(s) {
27
- if (s) {
28
- this.output += ' '.repeat(this.indent * 2) + s;
29
- }
47
+ if (s) {
48
+ return this.output += ' '.repeat(this.indent * 2) + s;
49
+ }
30
50
  }
51
+
31
52
  emitLine(s = '') {
32
- if (s) {
33
- this.output += ' '.repeat(this.indent * 2) + s;
34
- }
35
- this.output += '\n';
36
- this.outputLine++;
53
+ if (s) {
54
+ this.output += ' '.repeat(this.indent * 2) + s;
55
+ }
56
+ this.output += '\n';
57
+ return this.outputLine++;
37
58
  }
59
+
38
60
  mapSource(loc) {
39
- if (!this.sourceMapGenerator || !loc)
40
- return;
41
- const outputCol = this.indent * 2;
42
- this.sourceMapGenerator.addMapping({
43
- generated: { line: this.outputLine + 1, column: outputCol },
44
- original: { line: loc.line + 1, column: loc.column },
45
- source: this.options.filename || 'source.coffeehaml',
46
- });
47
- }
61
+ var outputCol;
62
+ if (!(this.sourceMapGenerator && loc)) {
63
+ return;
64
+ }
65
+ outputCol = this.indent * 2;
66
+ return this.sourceMapGenerator.addMapping({
67
+ generated: {
68
+ line: this.outputLine + 1,
69
+ column: outputCol
70
+ },
71
+ original: {
72
+ line: loc.line + 1,
73
+ column: loc.column
74
+ },
75
+ source: this.options.filename || 'source.coffeehaml'
76
+ });
77
+ }
78
+
48
79
  indentBlock(fn) {
49
- this.indent++;
50
- fn();
51
- this.indent--;
52
- }
53
- }
80
+ this.indent++;
81
+ fn();
82
+ return this.indent--;
83
+ }
84
+
85
+ };
86
+
87
+ EmitState.prototype.indent = 0;
88
+
89
+ EmitState.prototype.output = '';
90
+
91
+ EmitState.prototype.needsFragment = false;
92
+
93
+ EmitState.prototype.needsImport = false;
94
+
95
+ EmitState.prototype.options = null;
96
+
97
+ EmitState.prototype.sourceMapGenerator = null;
98
+
99
+ EmitState.prototype.outputLine = 0;
100
+
101
+ EmitState.prototype.warnings = [];
102
+
103
+ EmitState.prototype.hoisted = [];
104
+
105
+ EmitState.prototype.directEmit = false;
106
+
107
+ return EmitState;
108
+
109
+ }).call(this);
110
+
54
111
  // ─── Public API ────────────────────────────────────────────
55
- export function emit(ast, options = {}) {
56
- const state = new EmitState(options);
57
- // Emit import (always needed)
58
- state.emitLine(`import { jsx, jsxs, Fragment } from "${options.jsxRuntime || 'react/jsx-runtime'}";`);
59
- // Emit prologue lines (verbatim JS before any HAML)
60
- if (ast.prologue && ast.prologue.length > 0) {
61
- for (const line of ast.prologue) {
62
- state.emitLine(line);
63
- }
64
- }
65
- state.emitLine();
66
- // Component / HOC wrapping
67
- const wrap = options.wrap;
68
- if (wrap && wrap !== 'none') {
69
- const name = options.componentName || 'Component';
70
- const hocs = wrap === 'component' ? [] : wrap === 'observer' ? ['observer'] : wrap;
71
- const bodyExpr = compileComponentBody(ast, state);
72
- const inner = `function ${name}(props) { return ${bodyExpr}; }`;
73
- const wrapped = hocs.reduceRight((acc, hoc) => `${hoc}(${acc})`, inner);
74
- state.emitLine(`export default ${wrapped}`);
75
- }
76
- else {
77
- state.directEmit = true;
78
- emitNodes(ast.children, state, true);
79
- }
80
- const result = { code: state.output, warnings: state.warnings };
81
- if (state.sourceMapGenerator) {
82
- result.sourceMap = state.sourceMapGenerator.toString();
83
- }
84
- return result;
85
- }
86
- // ─── Node Emitters ─────────────────────────────────────────
112
+ export var emit = function(ast, options = {}) {
113
+ var bodyExpr, hocs, inner, j, len, line, name, ref, result, state, wrap, wrapped;
114
+ state = new EmitState(options);
115
+ state.emitLine(`import { jsx, jsxs, Fragment } from \"${options.jsxRuntime || 'react/jsx-runtime'}\";`);
116
+ // Emit prologue lines
117
+ if (ast.prologue && ast.prologue.length > 0) {
118
+ ref = ast.prologue;
119
+ for (j = 0, len = ref.length; j < len; j++) {
120
+ line = ref[j];
121
+ state.emitLine(line);
122
+ }
123
+ }
124
+ state.emitLine();
125
+ wrap = options.wrap;
126
+ if (wrap && wrap !== 'none') {
127
+ name = options.componentName || 'Component';
128
+ hocs = (function() {
129
+ switch (wrap) {
130
+ case 'component':
131
+ return [];
132
+ case 'observer':
133
+ return ['observer'];
134
+ default:
135
+ return wrap;
136
+ }
137
+ })();
138
+ bodyExpr = compileComponentBody(ast, state);
139
+ inner = `function ${name}(props) { return ${bodyExpr}; }`;
140
+ wrapped = hocs.reduceRight((function(acc, hoc) {
141
+ return `${hoc}(${acc})`;
142
+ }), inner);
143
+ state.emitLine(`export default ${wrapped}`);
144
+ } else {
145
+ state.directEmit = true;
146
+ emitNodes(ast.children, state, true);
147
+ }
148
+ result = {
149
+ code: state.output,
150
+ warnings: state.warnings
151
+ };
152
+ if (state.sourceMapGenerator) {
153
+ result.sourceMap = state.sourceMapGenerator.toString();
154
+ }
155
+ return result;
156
+ };
157
+
87
158
  // ─── Component Body Compilation ─────────────────────────────
88
- /** Emit all root children as a single JSX expression for component return. */
89
- function compileComponentBody(ast, state) {
90
- // Emit to a temporary buffer to capture statement-level output
91
- const tmpState = new EmitState(state.options);
92
- tmpState.indent = state.indent;
93
- // emitNodes handles control flow consuming subsequent siblings
94
- emitNodes(ast.children, tmpState, true);
95
- // Transfer warnings and hoisted statements from temp state
96
- state.warnings.push(...tmpState.warnings);
97
- const body = tmpState.output.trim();
98
- // Hoisted statements (from - blocks) go before the return
99
- const hoistedJs = tmpState.hoisted.length > 0
100
- ? tmpState.hoisted.join('; ') + '; '
101
- : '';
102
- if (!body) {
103
- if (hoistedJs)
104
- return `(() => { ${hoistedJs}return null; })()`;
105
- return 'null';
106
- }
107
- // Split into JS statements respecting brackets and strings
108
- const stmts = splitJsStatements(body);
109
- const jsx = stmts.length === 0 ? 'null'
110
- : stmts.length === 1 ? stmts[0]
111
- : `jsxs(Fragment, { children: [${stmts.join(', ')}] })`;
159
+ compileComponentBody = function(ast, state) {
160
+ var body, hoistedJs, jsx, stmts, tmpState;
161
+ tmpState = new EmitState(state.options);
162
+ tmpState.indent = state.indent;
163
+ emitNodes(ast.children, tmpState, true);
164
+ state.warnings.push(...tmpState.warnings);
165
+ body = tmpState.output.trim();
166
+ hoistedJs = tmpState.hoisted.length > 0 ? tmpState.hoisted.join('; ') + '; ' : '';
167
+ if (!body) {
112
168
  if (hoistedJs) {
113
- return `(() => { ${hoistedJs}return ${jsx}; })()`;
169
+ return `(()-> { ${hoistedJs}return null; })()`;
114
170
  }
115
- return jsx;
116
- }
117
- /** Split JS body into top-level statements, respecting string literals
118
- * and bracket depth. Only splits on `;` at depth 0 followed by newline. */
119
- function splitJsStatements(body) {
120
- const stmts = [];
121
- let depth = 0; // {} [] ()
122
- let inString = null;
123
- let start = 0;
124
- for (let i = 0; i < body.length; i++) {
125
- const ch = body[i];
126
- if (inString) {
127
- if (ch === '\\') {
128
- i++;
129
- continue;
130
- }
131
- if (ch === inString) {
132
- inString = null;
133
- }
134
- continue;
135
- }
136
- if (ch === "'" || ch === '"' || ch === '`') {
137
- inString = ch;
138
- continue;
139
- }
140
- if (ch === '{' || ch === '[' || ch === '(') {
141
- depth++;
142
- continue;
143
- }
144
- if (ch === '}' || ch === ']' || ch === ')') {
145
- depth--;
146
- continue;
147
- }
148
- if (ch === ';' && depth === 0 && body[i + 1] === '\n') {
149
- let stmt = body.slice(start, i).trim();
150
- if (stmt)
151
- stmts.push(stmt);
152
- start = i + 2; // skip ;\n
153
- i++; // skip \n
154
- }
155
- }
156
- // Trailing content after last ;
157
- const last = body.slice(start).trim();
158
- if (last) {
159
- stmts.push(last.endsWith(';') ? last.slice(0, -1).trim() : last);
160
- }
161
- return stmts;
162
- }
171
+ return 'null';
172
+ }
173
+ stmts = splitJsStatements(body);
174
+ jsx = stmts.length === 0 ? 'null' : stmts.length === 1 ? stmts[0] : `jsxs(Fragment, { children: [${stmts.join(', ')}] })`;
175
+ if (hoistedJs) {
176
+ return `(()-> { ${hoistedJs}return ${jsx}; })()`;
177
+ }
178
+ return jsx;
179
+ };
180
+
181
+ splitJsStatements = function(body) {
182
+ var ch, depth, i, inString, last, start, stmt, stmts;
183
+ stmts = [];
184
+ depth = 0;
185
+ inString = null;
186
+ start = 0;
187
+ i = 0;
188
+ while (i < body.length) {
189
+ ch = body[i];
190
+ if (inString) {
191
+ if (ch === '\\') {
192
+ i++;
193
+ continue;
194
+ }
195
+ if (ch === inString) {
196
+ inString = null;
197
+ }
198
+ i++;
199
+ continue;
200
+ }
201
+ if (ch === "'" || ch === '"' || ch === '`') {
202
+ inString = ch;
203
+ i++;
204
+ continue;
205
+ }
206
+ if (ch === '{' || ch === '[' || ch === '(') {
207
+ depth++;
208
+ i++;
209
+ continue;
210
+ }
211
+ if (ch === '}' || ch === ']' || ch === ')') {
212
+ depth--;
213
+ i++;
214
+ continue;
215
+ }
216
+ if (ch === ';' && depth === 0 && body[i + 1] === '\n') {
217
+ stmt = body.slice(start, i).trim();
218
+ if (stmt) {
219
+ stmts.push(stmt);
220
+ }
221
+ start = i + 2;
222
+ i++;
223
+ }
224
+ i++;
225
+ }
226
+ last = body.slice(start).trim();
227
+ if (last) {
228
+ stmts.push(last.endsWith(';') ? last.slice(0, -1).trim() : last);
229
+ }
230
+ return stmts;
231
+ };
232
+
163
233
  // ─── Node Emitters ─────────────────────────────────────────
164
- function emitNodes(nodes, state, isRoot = false) {
165
- for (let i = 0; i < nodes.length; i++) {
166
- const node = nodes[i];
167
- if (node instanceof ControlFlow) {
168
- // Control flow wraps subsequent siblings
169
- const remaining = nodes.slice(i + 1);
170
- emitControlFlow(node, remaining, state, isRoot);
171
- return; // control flow consumes remaining nodes
172
- }
173
- emitNode(node, state, isRoot);
174
- }
175
- }
176
- function emitNode(node, state, isRoot) {
177
- if (node instanceof Element)
178
- emitElement(node, state);
179
- else if (node instanceof ImplicitDiv)
180
- emitImplicitDiv(node, state);
181
- else if (node instanceof Text)
182
- emitText(node, state);
183
- else if (node instanceof Output)
184
- emitOutput(node, state);
185
- else if (node instanceof ControlFlow)
186
- emitControlFlow(node, [], state, isRoot);
187
- else if (node instanceof Comment)
188
- emitComment(node, state);
189
- else if (node instanceof Filter)
190
- emitFilter(node, state);
191
- else if (node instanceof Doctype)
192
- emitDoctype(node, state);
193
- }
234
+ emitNodes = function(nodes, state, isRoot = false) {
235
+ var i, node, remaining;
236
+ i = 0;
237
+ while (i < nodes.length) {
238
+ node = nodes[i];
239
+ if (node instanceof ControlFlow) {
240
+ remaining = nodes.slice(i + 1);
241
+ emitControlFlow(node, remaining, state, isRoot);
242
+ return;
243
+ } else {
244
+ emitNode(node, state, isRoot);
245
+ }
246
+ i++;
247
+ }
248
+ };
249
+
250
+ emitNode = function(node, state, isRoot) {
251
+ if (node instanceof Element) {
252
+ return emitElement(node, state);
253
+ } else if (node instanceof ImplicitDiv) {
254
+ return emitImplicitDiv(node, state);
255
+ } else if (node instanceof Text) {
256
+ return emitText(node, state);
257
+ } else if (node instanceof Output) {
258
+ return emitOutput(node, state);
259
+ } else if (node instanceof ControlFlow) {
260
+ return emitControlFlow(node, [], state, isRoot);
261
+ } else if (node instanceof Comment) {
262
+ return emitComment(node, state);
263
+ } else if (node instanceof Filter) {
264
+ return emitFilter(node, state);
265
+ } else if (node instanceof Doctype) {
266
+ return emitDoctype(node, state);
267
+ }
268
+ };
269
+
194
270
  // ─── Element ───────────────────────────────────────────────
195
- function emitElement(el, state) {
196
- const isVoid = typeof el.tag === 'string' && VOID_ELEMENTS.has(el.tag);
197
- const hasChildren = !isVoid && el.children.length > 0;
198
- // Tag
199
- const tagJs = tagToJs(el);
200
- // Attributes
201
- const attrResult = buildAttributes(el);
202
- const attrs = attrResult.parts;
203
- const keyArg = attrResult.keyExpr ? `, ${attrResult.keyExpr}` : '';
204
- const attrsJs = attrs.length > 0 ? `{ ${attrs.join(', ')} }` : 'null';
205
- // Children — embedded in props per React JSX runtime convention
271
+ emitElement = function(el, state) {
272
+ var allAttrs, attrResult, attrs, attrsJs, childJs, childParts, childrenAttr, hasChildren, isVoid, keyArg, tagJs;
273
+ mapSourceLocation(el.location, state);
274
+ isVoid = typeof el.tag === 'string' && VOID_ELEMENTS.has(el.tag);
275
+ hasChildren = !isVoid && el.children.length > 0;
276
+ tagJs = tagToJs(el);
277
+ attrResult = buildAttributes(el);
278
+ attrs = attrResult.parts;
279
+ keyArg = attrResult.keyExpr ? `, ${attrResult.keyExpr}` : '';
280
+ if (hasChildren) {
281
+ if (el.children.length === 1) {
282
+ childJs = emitChildToJs(el.children[0], state);
283
+ childrenAttr = `children: ${childJs}`;
284
+ allAttrs = attrs.length > 0 ? `{ ${attrs.join(', ')}, ${childrenAttr} }` : `{ ${childrenAttr} }`;
285
+ return state.emitLine(`jsx(${tagJs}, ${allAttrs}${keyArg});`);
286
+ } else {
287
+ childParts = el.children.map(function(c) {
288
+ return emitChildToJs(c, state);
289
+ });
290
+ childrenAttr = `children: [${childParts.join(', ')}]`;
291
+ allAttrs = attrs.length > 0 ? `{ ${attrs.join(', ')}, ${childrenAttr} }` : `{ ${childrenAttr} }`;
292
+ return state.emitLine(`jsxs(${tagJs}, ${allAttrs}${keyArg});`);
293
+ }
294
+ } else {
295
+ attrsJs = attrs.length > 0 ? `{ ${attrs.join(', ')} }` : 'null';
296
+ return state.emitLine(`jsx(${tagJs}, ${attrsJs}${keyArg});`);
297
+ }
298
+ };
299
+
300
+ tagToJs = function(el) {
301
+ if (el.tag instanceof Expression) {
302
+ return compileExpression(el.tag, el.location);
303
+ }
304
+ if (el.isComponent) {
305
+ return el.tag;
306
+ }
307
+ return `\"${el.tag}\"`;
308
+ };
309
+
310
+ buildAttributes = function(el) {
311
+ var attr, classNames, filteredParts, j, joined, k, keyExpr, len, len1, name, p, parts, ref, unique, valJs;
312
+ parts = [];
313
+ if (el.id) {
314
+ parts.push(`id: \"${el.id}\"`);
315
+ }
316
+ classNames = el.classes.slice();
317
+ ref = el.attributes;
318
+ for (j = 0, len = ref.length; j < len; j++) {
319
+ attr = ref[j];
320
+ if (attr.spread) {
321
+ valJs = compileExpression(attr.expression);
322
+ parts.push(`...${valJs}`);
323
+ continue;
324
+ }
325
+ valJs = compileExpression(attr.value);
326
+ name = attrNameToJs(attr.name);
327
+ if (name === 'className' || name === 'class') {
328
+ if (attr.shorthand) {
329
+ classNames.push(`$\{${valJs}}`);
330
+ } else {
331
+ classNames.push(valJs.replace(/^['"]|['"]$/g, ''));
332
+ }
333
+ } else {
334
+ parts.push(`${name}: ${valJs}`);
335
+ }
336
+ }
337
+ if (classNames.length > 0) {
338
+ unique = [...new Set(classNames)];
339
+ if (unique.length === 1 && !unique[0].includes('${')) {
340
+ parts.push(`className: \"${unique[0]}\"`);
341
+ } else {
342
+ joined = unique.map(function(c) {
343
+ if (c.includes('${')) {
344
+ return c;
345
+ } else {
346
+ return `\"${c}\"`;
347
+ }
348
+ }).join(' + " " + ');
349
+ parts.push(`className: ${joined}`);
350
+ }
351
+ }
352
+ // Extract key
353
+ keyExpr = null;
354
+ filteredParts = [];
355
+ for (k = 0, len1 = parts.length; k < len1; k++) {
356
+ p = parts[k];
357
+ if (p.startsWith('key: ')) {
358
+ keyExpr = p.slice(5).trim();
359
+ } else {
360
+ filteredParts.push(p);
361
+ }
362
+ }
363
+ return {
364
+ parts: filteredParts,
365
+ keyExpr: keyExpr
366
+ };
367
+ };
368
+
369
+ attrNameToJs = function(name) {
370
+ if (name === 'class') {
371
+ return 'className';
372
+ }
373
+ if (name === 'for') {
374
+ return 'htmlFor';
375
+ }
376
+ if (name.includes('-')) {
377
+ return `\"${name}\"`;
378
+ }
379
+ return name;
380
+ };
381
+
382
+ emitImplicitDiv = function(div, state) {
383
+ var el;
384
+ mapSourceLocation(div.location, state);
385
+ el = new Element('div', {
386
+ classes: div.classes,
387
+ id: div.id,
388
+ attributes: div.attributes,
389
+ children: div.children,
390
+ isComponent: false,
391
+ isSelfClosing: false,
392
+ location: div.location
393
+ });
394
+ return emitElement(el, state);
395
+ };
396
+
397
+ // ─── Child Emission ────────────────────────────────────────
398
+ emitChildToJs = function(node, state) {
399
+ var allAttrs, attrResult, attrs, attrsJs, c, childParts, childrenAttr, el, exprSource, fn, hasChildren, keyArg, tagJs, textContinuations;
400
+ if (node instanceof Text) {
401
+ return escapeString(node.value);
402
+ }
403
+ if (node instanceof Output) {
404
+ // Arrow continuation
405
+ if (node.children.length > 0 && /->\s*$/.test(node.expression.source.trim())) {
406
+ return emitArrowOutputToJs(node.expression.source, node.children, state);
407
+ }
408
+ textContinuations = (function() {
409
+ var j, len, ref, results;
410
+ ref = node.children;
411
+ results = [];
412
+ for (j = 0, len = ref.length; j < len; j++) {
413
+ c = ref[j];
414
+ if (c instanceof Text) {
415
+ results.push(c.value);
416
+ }
417
+ }
418
+ return results;
419
+ })();
420
+ exprSource = textContinuations.length > 0 ? [node.expression.source, ...textContinuations].join(' ') : node.expression.source;
421
+ return compileExpression(new Expression(exprSource), node.location);
422
+ }
423
+ if (node instanceof Element) {
424
+ hasChildren = node.children.length > 0;
425
+ tagJs = tagToJs(node);
426
+ attrResult = buildAttributes(node);
427
+ attrs = attrResult.parts;
428
+ keyArg = attrResult.keyExpr ? `, ${attrResult.keyExpr}` : '';
206
429
  if (hasChildren) {
207
- if (el.children.length === 1) {
208
- const childJs = emitChildToJs(el.children[0], state);
209
- const childrenAttr = `children: ${childJs}`;
210
- const allAttrs = attrs.length > 0 ? `{ ${attrs.join(', ')}, ${childrenAttr} }` : `{ ${childrenAttr} }`;
211
- state.emitLine(`jsx(${tagJs}, ${allAttrs}${keyArg});`);
212
- }
213
- else {
214
- const childParts = el.children.map(c => emitChildToJs(c, state));
215
- const childrenAttr = `children: [${childParts.join(', ')}]`;
216
- const allAttrs = attrs.length > 0 ? `{ ${attrs.join(', ')}, ${childrenAttr} }` : `{ ${childrenAttr} }`;
217
- state.emitLine(`jsxs(${tagJs}, ${allAttrs}${keyArg});`);
218
- }
219
- }
220
- else {
221
- state.emitLine(`jsx(${tagJs}, ${attrsJs}${keyArg});`);
222
- }
223
- }
224
- function tagToJs(el) {
225
- if (el.tag instanceof Expression) {
226
- return compileExpression(el.tag, el.location);
227
- }
228
- if (el.isComponent) {
229
- return el.tag;
230
- }
231
- return `"${el.tag}"`;
232
- }
233
- function buildAttributes(el) {
234
- const parts = [];
235
- // id
236
- if (el.id) {
237
- parts.push(`id: "${el.id}"`);
238
- }
239
- // class from .class modifiers
240
- let classNames = [...el.classes];
241
- // Named attributes
242
- for (const attr of el.attributes) {
243
- // Spread attribute: {...props}
244
- if (attr.spread) {
245
- const valJs = compileExpression(attr.expression);
246
- parts.push(`...${valJs}`);
247
- continue;
248
- }
249
- const valJs = compileExpression(attr.value);
250
- const name = attrNameToJs(attr.name);
251
- if (name === 'className' || name === 'class') {
252
- // Collect class names from className/class attribute
253
- if (attr.shorthand) {
254
- classNames.push(`\${${valJs}}`);
255
- }
256
- else {
257
- classNames.push(valJs.replace(/^['"]|['"]$/g, ''));
258
- }
259
- }
260
- else if (attr.shorthand) {
261
- parts.push(`${name}: ${valJs}`);
262
- }
263
- else {
264
- parts.push(`${name}: ${valJs}`);
265
- }
266
- }
267
- // Emit className
268
- if (classNames.length > 0) {
269
- const unique = [...new Set(classNames)];
270
- if (unique.length === 1 && !unique[0].includes('${')) {
271
- parts.push(`className: "${unique[0]}"`);
272
- }
273
- else {
274
- const joined = unique.map(c => c.includes('${') ? c : `"${c}"`).join(' + " " + ');
275
- parts.push(`className: ${joined}`);
276
- }
277
- }
278
- // style as object
279
- const styleAttr = el.attributes.find(a => !a.spread && attrNameToJs(a.name) === 'style');
280
- if (styleAttr && !styleAttr.spread && !styleAttr.shorthand) {
281
- // Already handled above — but ensure it's an object expression
282
- }
283
- // Extract key from parts for React 3rd-arg convention
284
- let keyExpr = null;
285
- const filteredParts = [];
286
- for (const p of parts) {
287
- if (p.startsWith('key: ')) {
288
- keyExpr = p.slice(5).trim();
289
- }
290
- else {
291
- filteredParts.push(p);
292
- }
293
- }
294
- return { parts: filteredParts, keyExpr };
295
- }
296
- function attrNameToJs(name) {
297
- // class → className for React
298
- if (name === 'class')
299
- return 'className';
300
- // for → htmlFor for React
301
- if (name === 'for')
302
- return 'htmlFor';
303
- // data-* attributes keep their name
304
- if (name.includes('-')) {
305
- // Convert to camelCase quoted: 'data-value' → "data-value"
306
- return `"${name}"`;
307
- }
308
- return name;
309
- }
310
- function emitImplicitDiv(div, state) {
311
- mapSourceLocation(div.location, state);
312
- const el = new Element('div', {
313
- classes: div.classes,
314
- id: div.id,
315
- attributes: div.attributes,
316
- children: div.children,
317
- isComponent: false,
318
- isSelfClosing: false,
319
- location: div.location,
430
+ childParts = node.children.map(function(c) {
431
+ return emitChildToJs(c, state);
432
+ });
433
+ childrenAttr = childParts.length === 1 ? `children: ${childParts[0]}` : `children: [${childParts.join(', ')}]`;
434
+ fn = childParts.length === 1 ? 'jsx' : 'jsxs';
435
+ allAttrs = attrs.length > 0 ? `{ ${attrs.join(', ')}, ${childrenAttr} }` : `{ ${childrenAttr} }`;
436
+ return `${fn}(${tagJs}, ${allAttrs}${keyArg})`;
437
+ }
438
+ attrsJs = attrs.length > 0 ? `{ ${attrs.join(', ')} }` : 'null';
439
+ return `jsx(${tagJs}, ${attrsJs}${keyArg})`;
440
+ }
441
+ if (node instanceof ImplicitDiv) {
442
+ el = new Element('div', {
443
+ classes: node.classes,
444
+ id: node.id,
445
+ attributes: node.attributes,
446
+ children: node.children,
447
+ isComponent: false,
448
+ isSelfClosing: false,
449
+ location: node.location
320
450
  });
321
- emitElement(el, state);
322
- }
323
- // ─── Child Emission ────────────────────────────────────────
324
- function emitChildToJs(node, state) {
325
- if (node instanceof Text) {
326
- return escapeString(node.value);
327
- }
328
- if (node instanceof Output) {
329
- // Arrow continuation: = expr -> with indented element children
330
- if (node.children.length > 0 && /->\s*$/.test(node.expression.source.trim())) {
331
- return emitArrowOutputToJs(node.expression.source, node.children, state);
332
- }
333
- // TEXT continuation: join text children into expression
334
- const textContinuations = node.children
335
- .filter(c => c instanceof Text)
336
- .map(c => c.value);
337
- const exprSource = textContinuations.length > 0
338
- ? [node.expression.source, ...textContinuations].join(' ')
339
- : node.expression.source;
340
- return compileExpression(new Expression(exprSource), node.location);
341
- }
342
- if (node instanceof Element) {
343
- const hasChildren = node.children.length > 0;
344
- const tagJs = tagToJs(node);
345
- const attrResult = buildAttributes(node);
346
- const attrs = attrResult.parts;
347
- const keyArg = attrResult.keyExpr ? `, ${attrResult.keyExpr}` : '';
348
- if (hasChildren) {
349
- const childParts = node.children.map(c => emitChildToJs(c, state));
350
- const childrenAttr = childParts.length === 1 ? `children: ${childParts[0]}` : `children: [${childParts.join(', ')}]`;
351
- const fn = childParts.length === 1 ? 'jsx' : 'jsxs';
352
- const allAttrs = attrs.length > 0 ? `{ ${attrs.join(', ')}, ${childrenAttr} }` : `{ ${childrenAttr} }`;
353
- return `${fn}(${tagJs}, ${allAttrs}${keyArg})`;
354
- }
355
- const attrsJs = attrs.length > 0 ? `{ ${attrs.join(', ')} }` : 'null';
356
- return `jsx(${tagJs}, ${attrsJs}${keyArg})`;
357
- }
358
- if (node instanceof ImplicitDiv) {
359
- const el = new Element('div', {
360
- classes: node.classes,
361
- id: node.id,
362
- attributes: node.attributes,
363
- children: node.children,
364
- isComponent: false,
365
- isSelfClosing: false,
366
- location: node.location,
367
- });
368
- return emitChildToJs(el, state);
369
- }
370
- if (node instanceof ControlFlow) {
371
- return emitControlFlowToJs(node, state);
372
- }
373
- if (node instanceof Filter) {
374
- return emitFilterToJs(node, state);
375
- }
376
- return 'null';
377
- }
451
+ return emitChildToJs(el, state);
452
+ }
453
+ if (node instanceof ControlFlow) {
454
+ return emitControlFlowToJs(node, state);
455
+ }
456
+ if (node instanceof Filter) {
457
+ return emitFilterToJs(node, state);
458
+ }
459
+ return 'null';
460
+ };
461
+
378
462
  // ─── Control Flow ──────────────────────────────────────────
379
- function emitControlFlow(cf, remaining, state, _isRoot) {
380
- if (cf.controlKind === 'statement') {
381
- emitStatement(cf, remaining, state);
382
- }
383
- else if (cf.isLoop) {
384
- emitLoop(cf, remaining, state);
385
- }
386
- else {
387
- emitConditional(cf, remaining, state);
388
- }
389
- }
390
- /** Emit an arbitrary CoffeeScript statement (- code). */
391
- function emitStatement(cf, remaining, state) {
392
- // Compile as statement, preserving var→const declarations
393
- const js = compileStatement(cf.expression.source.trim(), cf.location);
394
- // Indented children without a '-' prefix are code continuation lines
395
- // (lexed as TEXT) or nested HAML elements
396
- const stmts = [js];
397
- for (const child of cf.children) {
398
- if (child instanceof Text) {
399
- stmts.push(compileStatement(child.value, child.location));
400
- }
401
- else {
402
- emitNode(child, state, false);
403
- }
404
- }
405
- if (state.directEmit) {
406
- for (const s of stmts)
407
- state.emitLine(s + ';');
408
- }
409
- else {
410
- state.hoisted.push(...stmts);
411
- }
412
- if (remaining.length > 0) {
413
- emitNodes(remaining, state);
414
- }
415
- }
416
- function emitLoop(cf, remaining, state) {
417
- // - for item in items → items.map(item => ...)
418
- // - for item, i in items → items.map((item, i) => ...)
419
- // - while condition → (() => { const __r = []; while (cond) __r.push(...); return __r; })()
420
- if (cf.controlKind === 'for') {
421
- const expr = cf.expression.source.trim();
422
- const forMatch = expr.match(/^(.+?)\s+in\s+(.+)$/);
423
- if (forMatch) {
424
- const vars = forMatch[1].trim(); // e.g., "item" or "item, index"
425
- const iterable = forMatch[2].trim();
426
- state.emit(`(${compileExpression(new Expression(iterable), cf.location)}).map((${vars}) => `);
427
- // Wrap children in Fragment if multiple
428
- if (cf.children.length === 0) {
429
- state.output += 'null';
430
- }
431
- else if (cf.children.length === 1) {
432
- const childJs = emitChildToJs(cf.children[0], state);
433
- state.output += childJs;
434
- }
435
- else {
436
- state.output += 'jsxs(Fragment, { children: [';
437
- if (cf.children.length > 0) {
438
- state.output += cf.children.map(c => emitChildToJs(c, state)).join(', ');
439
- }
440
- state.output += '] })';
441
- }
442
- state.output += ')';
443
- // Handle remaining nodes (they go after the map)
444
- if (remaining.length > 0) {
445
- state.output += ',\n';
446
- emitNodes(remaining, state);
447
- }
448
- else {
449
- state.output += ';\n';
450
- }
451
- return;
452
- }
453
- }
454
- if (cf.controlKind === 'while') {
455
- const cond = compileExpression(cf.expression, cf.location);
456
- state.emitLine(`(() => {`);
457
- state.indentBlock(() => {
458
- state.emitLine(`const __result = [];`);
459
- state.emitLine(`while (${cond}) {`);
460
- state.indentBlock(() => {
461
- if (cf.children.length === 1 && cf.children[0] instanceof Element) {
462
- state.emit(`__result.push(${emitChildToJs(cf.children[0], state)});\n`);
463
- }
464
- else {
465
- state.emit(`__result.push(jsxs(Fragment, { children: [${cf.children.map(c => emitChildToJs(c, state)).join(', ')}] }));\n`);
466
- }
467
- });
468
- state.emitLine(`}`);
469
- state.emitLine(`return __result;`);
470
- });
471
- state.emit(`})()`);
472
- if (remaining.length > 0) {
473
- state.output += ',\n';
474
- emitNodes(remaining, state);
475
- }
476
- else {
477
- state.output += ';\n';
478
- }
479
- return;
480
- }
481
- // Fallback
482
- state.emitLine(`/* TODO: emit loop ${cf.controlKind} */`);
483
- }
484
- function emitConditional(cf, remaining, state) {
485
- // else is always reached via cf.next chain — the " : " was emitted by parent
486
- if (cf.controlKind === 'else') {
487
- emitElseBranch(cf, state);
488
- return;
489
- }
490
- // Build ternary chain: cond ? jsx(...) : (elseCond ? jsx(...) : null)
491
- const condition = compileExpression(cf.expression, cf.location);
492
- if (cf.controlKind === 'unless') {
493
- // unless cond → !(cond)
494
- state.emit(`!(${condition}) ? `);
495
- }
496
- else {
497
- state.emit(`${condition} ? `);
498
- }
499
- // Then branch
500
- if (cf.children.length === 0) {
501
- state.output += 'null';
502
- }
503
- else if (cf.children.length === 1) {
504
- state.output += emitChildToJs(cf.children[0], state);
505
- }
506
- else {
507
- state.output += `jsxs(Fragment, { children: [${cf.children.map(c => emitChildToJs(c, state)).join(', ')}] })`;
508
- }
509
- // Else / remaining
510
- state.output += ' : ';
511
- if (cf.next) {
512
- emitConditional(cf.next, [], state);
513
- }
514
- else if (remaining.length > 0) {
515
- if (remaining.length === 1) {
516
- state.output += emitChildToJs(remaining[0], state);
517
- }
518
- else {
519
- state.output += `jsxs(Fragment, { children: [${remaining.map(c => emitChildToJs(c, state)).join(', ')}] })`;
520
- }
521
- }
522
- else {
523
- state.output += 'null';
524
- }
525
- state.output += ';\n';
526
- }
527
- /** Emit the body of an else branch — no condition prefix. */
528
- function emitElseBranch(cf, state) {
529
- if (cf.children.length === 0) {
463
+ emitControlFlow = function(cf, remaining, state, _isRoot) {
464
+ if (cf.controlKind === 'statement') {
465
+ return emitStatement(cf, remaining, state);
466
+ } else if (cf.isLoop()) {
467
+ return emitLoop(cf, remaining, state);
468
+ } else {
469
+ return emitConditional(cf, remaining, state);
470
+ }
471
+ };
472
+
473
+ emitStatement = function(cf, remaining, state) {
474
+ var child, j, js, k, len, len1, ref, s, stmts;
475
+ js = compileStatement(cf.expression.source.trim(), cf.location);
476
+ stmts = [js];
477
+ ref = cf.children;
478
+ for (j = 0, len = ref.length; j < len; j++) {
479
+ child = ref[j];
480
+ if (child instanceof Text) {
481
+ stmts.push(compileStatement(child.value, child.location));
482
+ } else {
483
+ emitNode(child, state, false);
484
+ }
485
+ }
486
+ if (state.directEmit) {
487
+ for (k = 0, len1 = stmts.length; k < len1; k++) {
488
+ s = stmts[k];
489
+ state.emitLine(s + ';');
490
+ }
491
+ } else {
492
+ state.hoisted.push(...stmts);
493
+ }
494
+ if (remaining.length > 0) {
495
+ return emitNodes(remaining, state);
496
+ }
497
+ };
498
+
499
+ emitLoop = function(cf, remaining, state) {
500
+ var cond, expr, forMatch, iterable, vars;
501
+ if (cf.controlKind === 'for') {
502
+ expr = cf.expression.source.trim();
503
+ forMatch = expr.match(/^(.+?)\s+in\s+(.+)$/);
504
+ if (forMatch) {
505
+ vars = forMatch[1].trim();
506
+ iterable = forMatch[2].trim();
507
+ state.emit(`(${compileExpression(new Expression(iterable), cf.location)}).map((${vars}) => `);
508
+ if (cf.children.length === 0) {
530
509
  state.output += 'null';
531
- }
532
- else if (cf.children.length === 1) {
510
+ } else if (cf.children.length === 1) {
533
511
  state.output += emitChildToJs(cf.children[0], state);
534
- }
535
- else {
536
- state.output += `jsxs(Fragment, {}, ${cf.children.map(c => emitChildToJs(c, state)).join(', ')})`;
537
- }
538
- }
539
- function emitControlFlowToJs(cf, state) {
540
- // Inline version — used when control flow appears as a child
541
- if (cf.isLoop && cf.controlKind === 'for') {
542
- const expr = cf.expression.source.trim();
543
- const forMatch = expr.match(/^(.+?)\s+in\s+(.+)$/);
544
- if (forMatch) {
545
- const vars = forMatch[1].trim();
546
- const iterable = forMatch[2].trim();
547
- const compiledIterable = compileExpression(new Expression(iterable), cf.location);
548
- const body = cf.children.length === 1
549
- ? emitChildToJs(cf.children[0], state)
550
- : `jsxs(Fragment, { children: [${cf.children.map(c => emitChildToJs(c, state)).join(', ')}] })`;
551
- return `(${compiledIterable}).map((${vars}) => ${body})`;
552
- }
553
- }
554
- if (cf.controlKind === 'else') {
555
- // inline else — just emit the body
556
- return cf.children.length === 1
557
- ? emitChildToJs(cf.children[0], state)
558
- : cf.children.length > 0
559
- ? `jsxs(Fragment, {}, ${cf.children.map(c => emitChildToJs(c, state)).join(', ')})`
560
- : 'null';
561
- }
562
- if (cf.isConditional) {
563
- const condition = compileExpression(cf.expression, cf.location);
564
- const condExpr = cf.controlKind === 'unless' ? `!(${condition})` : condition;
565
- const consequent = cf.children.length === 1
566
- ? emitChildToJs(cf.children[0], state)
567
- : cf.children.length > 0
568
- ? `jsxs(Fragment, {}, ${cf.children.map(c => emitChildToJs(c, state)).join(', ')})`
569
- : 'null';
570
- const alternate = cf.next ? emitControlFlowToJs(cf.next, state) : 'null';
571
- return `${condExpr} ? ${consequent} : ${alternate}`;
572
- }
573
- if (cf.controlKind === 'statement') {
574
- // Inline statements can't be used as JSX children — pass through as comment
575
- const js = compileExpression(cf.expression, cf.location);
576
- return `/* - ${js} */ null`;
577
- }
578
- return 'null';
579
- }
512
+ } else {
513
+ state.output += `jsxs(Fragment, { children: [${cf.children.map(function(c) {
514
+ return emitChildToJs(c, state);
515
+ }).join(', ')}] })`;
516
+ }
517
+ state.output += ')';
518
+ if (remaining.length > 0) {
519
+ state.output += ',\n';
520
+ emitNodes(remaining, state);
521
+ } else {
522
+ state.output += ';\n';
523
+ }
524
+ return;
525
+ }
526
+ }
527
+ if (cf.controlKind === 'while') {
528
+ cond = compileExpression(cf.expression, cf.location);
529
+ state.emitLine('(() => {');
530
+ state.indentBlock(function() {
531
+ state.emitLine('const __result = [];');
532
+ state.emitLine(`while (${cond}) {`);
533
+ state.indentBlock(function() {
534
+ if (cf.children.length === 1 && cf.children[0] instanceof Element) {
535
+ return state.emit(`__result.push(${emitChildToJs(cf.children[0], state)});\n`);
536
+ } else {
537
+ return state.emit(`__result.push(jsxs(Fragment, { children: [${cf.children.map(function(c) {
538
+ return emitChildToJs(c, state);
539
+ }).join(', ')}] }));\n`);
540
+ }
541
+ });
542
+ state.emitLine('}');
543
+ return state.emitLine('return __result;');
544
+ });
545
+ state.emit('})()');
546
+ if (remaining.length > 0) {
547
+ state.output += ',\n';
548
+ emitNodes(remaining, state);
549
+ } else {
550
+ state.output += ';\n';
551
+ }
552
+ return;
553
+ }
554
+ return state.emitLine(`/* TODO: emit loop ${cf.controlKind} */`);
555
+ };
556
+
557
+ emitConditional = function(cf, remaining, state) {
558
+ var condition;
559
+ if (cf.controlKind === 'else') {
560
+ emitElseBranch(cf, state);
561
+ return;
562
+ }
563
+ condition = compileExpression(cf.expression, cf.location);
564
+ if (cf.controlKind === 'unless') {
565
+ state.emit(`!(${condition}) ? `);
566
+ } else {
567
+ state.emit(`${condition} ? `);
568
+ }
569
+ // Then branch
570
+ if (cf.children.length === 0) {
571
+ state.output += 'null';
572
+ } else if (cf.children.length === 1) {
573
+ state.output += emitChildToJs(cf.children[0], state);
574
+ } else {
575
+ state.output += `jsxs(Fragment, { children: [${cf.children.map(function(c) {
576
+ return emitChildToJs(c, state);
577
+ }).join(', ')}] })`;
578
+ }
579
+ state.output += ' : ';
580
+ if (cf.next) {
581
+ emitConditional(cf.next, [], state);
582
+ } else if (remaining.length > 0) {
583
+ if (remaining.length === 1) {
584
+ state.output += emitChildToJs(remaining[0], state);
585
+ } else {
586
+ state.output += `jsxs(Fragment, { children: [${remaining.map(function(c) {
587
+ return emitChildToJs(c, state);
588
+ }).join(', ')}] })`;
589
+ }
590
+ } else {
591
+ state.output += 'null';
592
+ }
593
+ return state.output += ';\n';
594
+ };
595
+
596
+ emitElseBranch = function(cf, state) {
597
+ if (cf.children.length === 0) {
598
+ return state.output += 'null';
599
+ } else if (cf.children.length === 1) {
600
+ return state.output += emitChildToJs(cf.children[0], state);
601
+ } else {
602
+ return state.output += `jsxs(Fragment, {}, ${cf.children.map(function(c) {
603
+ return emitChildToJs(c, state);
604
+ }).join(', ')})`;
605
+ }
606
+ };
607
+
608
+ emitControlFlowToJs = function(cf, state) {
609
+ var alternate, body, compiledIterable, condExpr, condition, consequent, expr, forMatch, iterable, js, vars;
610
+ if (cf.isLoop() && cf.controlKind === 'for') {
611
+ expr = cf.expression.source.trim();
612
+ forMatch = expr.match(/^(.+?)\s+in\s+(.+)$/);
613
+ if (forMatch) {
614
+ vars = forMatch[1].trim();
615
+ iterable = forMatch[2].trim();
616
+ compiledIterable = compileExpression(new Expression(iterable), cf.location);
617
+ body = cf.children.length === 1 ? emitChildToJs(cf.children[0], state) : `jsxs(Fragment, { children: [${cf.children.map(function(c) {
618
+ return emitChildToJs(c, state);
619
+ }).join(', ')}] })`;
620
+ return `(${compiledIterable}).map((${vars}) => ${body})`;
621
+ }
622
+ }
623
+ if (cf.controlKind === 'else') {
624
+ if (cf.children.length === 1) {
625
+ return emitChildToJs(cf.children[0], state);
626
+ } else if (cf.children.length > 0) {
627
+ return `jsxs(Fragment, {}, ${cf.children.map(function(c) {
628
+ return emitChildToJs(c, state);
629
+ }).join(', ')})`;
630
+ } else {
631
+ return 'null';
632
+ }
633
+ }
634
+ if (cf.isConditional()) {
635
+ condition = compileExpression(cf.expression, cf.location);
636
+ condExpr = cf.controlKind === 'unless' ? `!(${condition})` : condition;
637
+ consequent = cf.children.length === 1 ? emitChildToJs(cf.children[0], state) : cf.children.length > 0 ? `jsxs(Fragment, {}, ${cf.children.map(function(c) {
638
+ return emitChildToJs(c, state);
639
+ }).join(', ')})` : 'null';
640
+ alternate = cf.next ? emitControlFlowToJs(cf.next, state) : 'null';
641
+ return `${condExpr} ? ${consequent} : ${alternate}`;
642
+ }
643
+ if (cf.controlKind === 'statement') {
644
+ js = compileExpression(cf.expression, cf.location);
645
+ return `/* - ${js} */ null`;
646
+ }
647
+ return 'null';
648
+ };
649
+
580
650
  // ─── Text / Output / Comment / Filter / Doctype ────────────
581
- function emitText(text, state) {
582
- // Standalone text at module level — emit as string expression
583
- state.emitLine(`"${escapeString(text.value)}";`);
584
- }
585
- function emitOutput(out, state) {
586
- // Separate continuation children into TEXT (code lines to join) and elements
587
- const textLines = [];
588
- const elementChildren = [];
589
- for (const child of out.children) {
590
- if (child instanceof Text) {
591
- textLines.push(child.value);
592
- }
593
- else {
594
- elementChildren.push(child);
595
- }
596
- }
597
- // Build the full expression source, joining TEXT continuation lines
598
- let exprSource = out.expression.source;
599
- if (textLines.length > 0) {
600
- exprSource = [exprSource, ...textLines].join(' ');
601
- }
602
- // Arrow continuation: = items.map (item) -> with indented element children
603
- // Compiles to target.map((args) => <JSX>) — CoffeeScript arrow becomes JS arrow
604
- if (elementChildren.length > 0 && /->\s*$/.test(exprSource.trim())) {
605
- emitArrowOutput(exprSource, elementChildren, out, state);
606
- return;
607
- }
608
- const js = compileExpression(new Expression(exprSource), out.location);
609
- let result;
610
- if (out.outputKind === 'escaped') {
611
- result = `jsx(Fragment, { children: ${js} })`;
612
- }
613
- else {
614
- result = `jsx(Fragment, {}, ${js})`;
615
- }
616
- // Emit nested element children after the expression (siblings, not body)
617
- for (const child of elementChildren) {
618
- emitNode(child, state, false);
619
- }
620
- state.emitLine(result + ';');
621
- }
622
- /** Handle = expr -> with indented element children — compiles to an arrow function
623
- * returning JSX. E.g., = items.map (item) -> + indented %li → items.map((item) => <li/>) */
624
- function emitArrowOutput(exprSource, elementChildren, out, state) {
625
- // Parse: strip trailing ->, extract call target and arrow args
626
- const stripped = exprSource.replace(/\s*->\s*$/, '').trim();
627
- // Match pattern: target (args) — everything up to last parenthesized arg list
628
- const arrowMatch = stripped.match(/^(.+?)\s*\(([^)]*)\)\s*$/);
629
- let callTarget;
630
- let arrowArgs;
631
- if (arrowMatch) {
632
- callTarget = arrowMatch[1].trim(); // e.g., "items.map"
633
- arrowArgs = arrowMatch[2].trim(); // e.g., "item" or "item, i"
634
- }
635
- else {
636
- // No parens: = -> with just -> (bare arrow)
637
- callTarget = stripped;
638
- arrowArgs = '';
639
- }
640
- // Compile the call target as a CoffeeScript expression
641
- const compiledTarget = compileExpression(new Expression(callTarget), out.location);
642
- // Compile children as JSX body
643
- let body;
644
- if (elementChildren.length === 1) {
645
- body = emitChildToJs(elementChildren[0], state);
646
- }
647
- else {
648
- body = `jsxs(Fragment, { children: [${elementChildren.map(c => emitChildToJs(c, state)).join(', ')}] })`;
649
- }
650
- // Build the final expression
651
- const arrowFn = arrowArgs
652
- ? `(${arrowArgs}) => ${body}`
653
- : `() => ${body}`;
654
- const result = out.outputKind === 'escaped'
655
- ? `jsx(Fragment, { children: ${compiledTarget}(${arrowFn}) })`
656
- : `jsx(Fragment, {}, ${compiledTarget}(${arrowFn}))`;
657
- state.emitLine(result + ';');
658
- }
659
- /** Inline version of emitArrowOutput — returns JS string for use inside emitChildToJs. */
660
- function emitArrowOutputToJs(exprSource, elementChildren, state) {
661
- const stripped = exprSource.replace(/\s*->\s*$/, '').trim();
662
- const arrowMatch = stripped.match(/^(.+?)\s*\(([^)]*)\)\s*$/);
663
- let callTarget;
664
- let arrowArgs;
665
- if (arrowMatch) {
666
- callTarget = arrowMatch[1].trim();
667
- arrowArgs = arrowMatch[2].trim();
668
- }
669
- else {
670
- callTarget = stripped;
671
- arrowArgs = '';
672
- }
673
- const compiledTarget = compileExpression(new Expression(callTarget));
674
- let body;
675
- if (elementChildren.length === 1) {
676
- body = emitChildToJs(elementChildren[0], state);
677
- }
678
- else {
679
- body = `jsxs(Fragment, { children: [${elementChildren.map(c => emitChildToJs(c, state)).join(', ')}] })`;
680
- }
681
- const arrowFn = arrowArgs
682
- ? `(${arrowArgs}) => ${body}`
683
- : `() => ${body}`;
684
- return `${compiledTarget}(${arrowFn})`;
685
- }
686
- function emitComment(comment, state) {
687
- if (comment.commentKind === 'html') {
688
- state.emitLine(`/* <!-- ${comment.text} --> */`);
689
- }
690
- // Haml comments (-#) are stripped entirely
691
- }
692
- function emitFilter(filter, state) {
693
- const { filterName, content } = filter;
694
- // 1. Plugin-registered filter handlers take priority
695
- const handler = state.options.filters?.[filterName];
696
- if (handler) {
697
- const html = handler(content, filterName);
698
- state.emitLine(`jsx(Fragment, { dangerouslySetInnerHTML: { __html: ${JSON.stringify(html)} } })`);
699
- return;
700
- }
701
- // 2. Built-in passthrough filters
702
- switch (filterName) {
703
- case 'css':
704
- state.warnings.push({
705
- message: ':css filter emits raw CSS string — not usable as a <style> tag in React. Use a plugin handler to inject CSS properly.',
706
- location: filter.location,
707
- phase: 'emitter',
708
- });
709
- state.emitLine(`"${escapeString(content)}";`);
710
- break;
711
- case 'javascript':
712
- state.warnings.push({
713
- message: ':javascript filter emits raw JS at module level — an XSS risk with untrusted input. Ensure content is trusted.',
714
- location: filter.location,
715
- phase: 'emitter',
716
- });
717
- state.emitLine(content);
718
- break;
719
- case 'coffee':
720
- state.emitLine(content);
721
- break;
722
- default:
723
- state.warnings.push({
724
- message: `:${filterName} filter used but no handler registered. Add one to compilerOptions.filters.`,
725
- location: filter.location,
726
- phase: 'emitter',
727
- });
728
- state.emitLine(`"${escapeString(content)}";`);
729
- }
730
- }
731
- /** Emit a filter as a JS expression (for use as a child within parent elements). */
732
- function emitFilterToJs(filter, state) {
733
- const { filterName, content } = filter;
734
- const handler = state.options.filters?.[filterName];
735
- if (handler) {
736
- const html = handler(content, filterName);
737
- return `jsx(Fragment, { dangerouslySetInnerHTML: { __html: ${JSON.stringify(html)} } })`;
738
- }
739
- // No handler — emit raw text as a string child
740
- return escapeString(content);
741
- }
742
- function emitDoctype(doctype, state) {
743
- // Doctype is typically handled by the HTML renderer, but for React
744
- // we emit it as a comment or skip it
745
- state.emitLine(`// DOCTYPE ${doctype.value || 'html'}`);
746
- }
651
+ emitText = function(text, state) {
652
+ return state.emitLine(`\"${escapeString(text.value)}\";`);
653
+ };
654
+
655
+ emitOutput = function(out, state) {
656
+ var child, elementChildren, exprSource, j, js, k, len, len1, ref, result, textLines;
657
+ textLines = [];
658
+ elementChildren = [];
659
+ ref = out.children;
660
+ for (j = 0, len = ref.length; j < len; j++) {
661
+ child = ref[j];
662
+ if (child instanceof Text) {
663
+ textLines.push(child.value);
664
+ } else {
665
+ elementChildren.push(child);
666
+ }
667
+ }
668
+ exprSource = out.expression.source;
669
+ if (textLines.length > 0) {
670
+ exprSource = [exprSource, ...textLines].join(' ');
671
+ }
672
+ // Arrow continuation
673
+ if (elementChildren.length > 0 && /->\s*$/.test(exprSource.trim())) {
674
+ emitArrowOutput(exprSource, elementChildren, out, state);
675
+ return;
676
+ }
677
+ js = compileExpression(new Expression(exprSource), out.location);
678
+ result = out.outputKind === 'escaped' ? `jsx(Fragment, { children: ${js} })` : `jsx(Fragment, {}, ${js})`;
679
+ for (k = 0, len1 = elementChildren.length; k < len1; k++) {
680
+ child = elementChildren[k];
681
+ emitNode(child, state, false);
682
+ }
683
+ return state.emitLine(result + ';');
684
+ };
685
+
686
+ emitArrowOutput = function(exprSource, elementChildren, out, state) {
687
+ var arrowArgs, arrowFn, arrowMatch, body, callTarget, compiledTarget, result, stripped;
688
+ stripped = exprSource.replace(/\s*->\s*$/, '').trim();
689
+ arrowMatch = stripped.match(/^(.+?)\s*\(([^)]*)\)\s*$/);
690
+ if (arrowMatch) {
691
+ callTarget = arrowMatch[1].trim();
692
+ arrowArgs = arrowMatch[2].trim();
693
+ } else {
694
+ callTarget = stripped;
695
+ arrowArgs = '';
696
+ }
697
+ compiledTarget = compileExpression(new Expression(callTarget), out.location);
698
+ body = elementChildren.length === 1 ? emitChildToJs(elementChildren[0], state) : `jsxs(Fragment, { children: [${elementChildren.map(function(c) {
699
+ return emitChildToJs(c, state);
700
+ }).join(', ')}] })`;
701
+ arrowFn = arrowArgs ? `(${arrowArgs}) => ${body}` : `() => ${body}`;
702
+ result = out.outputKind === 'escaped' ? `jsx(Fragment, { children: ${compiledTarget}(${arrowFn}) })` : `jsx(Fragment, {}, ${compiledTarget}(${arrowFn}))`;
703
+ return state.emitLine(result + ';');
704
+ };
705
+
706
+ emitArrowOutputToJs = function(exprSource, elementChildren, state) {
707
+ var arrowArgs, arrowFn, arrowMatch, body, callTarget, compiledTarget, stripped;
708
+ stripped = exprSource.replace(/\s*->\s*$/, '').trim();
709
+ arrowMatch = stripped.match(/^(.+?)\s*\(([^)]*)\)\s*$/);
710
+ if (arrowMatch) {
711
+ callTarget = arrowMatch[1].trim();
712
+ arrowArgs = arrowMatch[2].trim();
713
+ } else {
714
+ callTarget = stripped;
715
+ arrowArgs = '';
716
+ }
717
+ compiledTarget = compileExpression(new Expression(callTarget));
718
+ body = elementChildren.length === 1 ? emitChildToJs(elementChildren[0], state) : `jsxs(Fragment, { children: [${elementChildren.map(function(c) {
719
+ return emitChildToJs(c, state);
720
+ }).join(', ')}] })`;
721
+ arrowFn = arrowArgs ? `(${arrowArgs}) => ${body}` : `() => ${body}`;
722
+ return `${compiledTarget}(${arrowFn})`;
723
+ };
724
+
725
+ emitComment = function(comment, state) {
726
+ if (comment.commentKind === 'html') {
727
+ return state.emitLine(`/* <!-- ${comment.text} --> */`);
728
+ }
729
+ };
730
+
731
+ emitFilter = function(filter, state) {
732
+ var content, filterName, handler, html, ref;
733
+ ({filterName, content} = filter);
734
+ handler = (ref = state.options.filters) != null ? ref[filterName] : void 0;
735
+ if (handler) {
736
+ html = handler(content, filterName);
737
+ state.emitLine(`jsx(Fragment, { dangerouslySetInnerHTML: { __html: ${JSON.stringify(html)} } })`);
738
+ return;
739
+ }
740
+ switch (filterName) {
741
+ case 'css':
742
+ state.warnings.push({
743
+ message: ':css filter emits raw CSS string — not usable as a <style> tag in React. Use a plugin handler to inject CSS properly.',
744
+ location: filter.location,
745
+ phase: 'emitter'
746
+ });
747
+ return state.emitLine(`\"${escapeString(content)}\";`);
748
+ case 'javascript':
749
+ state.warnings.push({
750
+ message: ':javascript filter emits raw JS at module level — an XSS risk with untrusted input. Ensure content is trusted.',
751
+ location: filter.location,
752
+ phase: 'emitter'
753
+ });
754
+ return state.emitLine(content);
755
+ case 'coffee':
756
+ return state.emitLine(content);
757
+ default:
758
+ state.warnings.push({
759
+ message: `:${filterName} filter used but no handler registered. Add one to compilerOptions.filters.`,
760
+ location: filter.location,
761
+ phase: 'emitter'
762
+ });
763
+ return state.emitLine(`\"${escapeString(content)}\";`);
764
+ }
765
+ };
766
+
767
+ emitFilterToJs = function(filter, state) {
768
+ var content, filterName, handler, html, ref;
769
+ ({filterName, content} = filter);
770
+ handler = (ref = state.options.filters) != null ? ref[filterName] : void 0;
771
+ if (handler) {
772
+ html = handler(content, filterName);
773
+ return `jsx(Fragment, { dangerouslySetInnerHTML: { __html: ${JSON.stringify(html)} } })`;
774
+ }
775
+ return escapeString(content);
776
+ };
777
+
778
+ emitDoctype = function(doctype, state) {
779
+ return state.emitLine(`// DOCTYPE ${doctype.value || 'html'}`);
780
+ };
781
+
747
782
  // ─── Helpers ───────────────────────────────────────────────
748
- function escapeString(s) {
749
- return JSON.stringify(s);
750
- }
751
- function mapSourceLocation(loc, state) {
752
- if (loc) {
753
- state.mapSource(loc.start);
754
- }
755
- }
756
- //# sourceMappingURL=emitter.js.map
783
+ escapeString = function(s) {
784
+ return JSON.stringify(s);
785
+ };
786
+
787
+ mapSourceLocation = function(loc, state) {
788
+ if (loc) {
789
+ return state.mapSource(loc.start);
790
+ }
791
+ };