coffeehaml 0.7.7 → 0.10.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.
package/README.md CHANGED
@@ -152,6 +152,9 @@ console.log(result.code);
152
152
  | `- code` | Arbitrary CoffeeScript statements; continuation via indented lines |
153
153
  | `- for x in xs` | Loop → `.map()` |
154
154
  | `- else`, `- else if` | Chain conditionals |
155
+ | `---` … `---` | Fenced CoffeeScript block — render-time statements (multiline `-`) |
156
+ | `===` … `===` | Fenced yield block — CoffeeScript whose final value renders (escaped) |
157
+ | `~~~` … `~~~` | Fenced preamble block — module-scope code (imports, helpers, setup) |
155
158
  | `-# comment` | Haml comment (stripped) |
156
159
  | `/ comment` | HTML comment |
157
160
  | `.wrapper` | Implicit div |
@@ -173,7 +176,7 @@ Full grammar: [`docs/grammar.md`](docs/grammar.md)
173
176
 
174
177
  ## Status
175
178
 
176
- **v0.7.0 — production beta.** The compiler pipeline (Lexer → Parser → Emitter)
179
+ **v0.10.0 — production beta.** The compiler pipeline (Lexer → Parser → Emitter)
177
180
  is complete. The [Prettier plugin](src/prettier/) provides 16 deactivatable
178
181
  formatting options including attribute style preservation, statement merging,
179
182
  blank line handling, and CoffeeScript code formatting.
@@ -194,6 +197,9 @@ blank line handling, and CoffeeScript code formatting.
194
197
  | Haml/HTML comments | ✅ |
195
198
  | `:filter` blocks | ✅ |
196
199
  | Prologue passthrough (`import`, `@decorator`) | ✅ |
200
+ | `---` render-time CoffeeScript block | ✅ |
201
+ | `===` yield block (fenced output) | ✅ |
202
+ | `~~~` module-scope preamble block | ✅ |
197
203
  | Component / HOC wrapping (`wrap: 'observer'`) | ✅ |
198
204
  | Vite plugin with HMR | ✅ |
199
205
  | CLI `--wrap` flag | ✅ |
package/dist/ast.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  } from './types.js';
7
7
 
8
8
  // ─── Node Kinds ────────────────────────────────────────────
9
- export var NodeKind = ['Document', 'Element', 'ImplicitDiv', 'Text', 'Output', 'ControlFlow', 'Comment', 'Filter', 'Doctype'];
9
+ export var NodeKind = ['Document', 'Element', 'ImplicitDiv', 'Text', 'Output', 'ControlFlow', 'Comment', 'Filter', 'CoffeeBlock', 'CoffeeYield', 'CoffeePreamble', 'Doctype'];
10
10
 
11
11
  export var ControlFlowKind = ['if', 'unless', 'for', 'while', 'else', 'statement'];
12
12
 
@@ -319,6 +319,71 @@ export var Filter = (function() {
319
319
 
320
320
  }).call(this);
321
321
 
322
+ export var CoffeeBlock = (function() {
323
+ // ─── CoffeeBlock ───────────────────────────────────────────
324
+ // A fenced raw CoffeeScript block (--- ... ---). Its content is a
325
+ // multi-line CoffeeScript program compiled to render-time statements — the
326
+ // multiline counterpart to `-`. It runs on every render and produces no
327
+ // output.
328
+ class CoffeeBlock extends Node {
329
+ constructor(content, location = null) {
330
+ super(location);
331
+ this.content = content;
332
+ }
333
+
334
+ };
335
+
336
+ CoffeeBlock.prototype.kind = 'CoffeeBlock';
337
+
338
+ CoffeeBlock.prototype.content = '';
339
+
340
+ return CoffeeBlock;
341
+
342
+ }).call(this);
343
+
344
+ export var CoffeePreamble = (function() {
345
+ // ─── CoffeePreamble ────────────────────────────────────────
346
+ // A fenced CoffeeScript preamble block (~~~ ... ~~~). The body is a
347
+ // multi-line CoffeeScript program compiled to statements hoisted to module
348
+ // scope — imports, helpers, and once-only setup. It runs once at import and
349
+ // produces no output.
350
+ class CoffeePreamble extends Node {
351
+ constructor(content, location = null) {
352
+ super(location);
353
+ this.content = content;
354
+ }
355
+
356
+ };
357
+
358
+ CoffeePreamble.prototype.kind = 'CoffeePreamble';
359
+
360
+ CoffeePreamble.prototype.content = '';
361
+
362
+ return CoffeePreamble;
363
+
364
+ }).call(this);
365
+
366
+ export var CoffeeYield = (function() {
367
+ // ─── CoffeeYield ───────────────────────────────────────────
368
+ // A fenced CoffeeScript yield block (=== ... ===). The body is a
369
+ // multi-line CoffeeScript program whose final value is yielded as
370
+ // content at that position (the multiline counterpart to `=`).
371
+ class CoffeeYield extends Node {
372
+ constructor(content, location = null) {
373
+ super(location);
374
+ this.content = content;
375
+ }
376
+
377
+ };
378
+
379
+ CoffeeYield.prototype.kind = 'CoffeeYield';
380
+
381
+ CoffeeYield.prototype.content = '';
382
+
383
+ return CoffeeYield;
384
+
385
+ }).call(this);
386
+
322
387
  export var Doctype = (function() {
323
388
  // ─── Doctype ───────────────────────────────────────────────
324
389
  class Doctype extends Node {
package/dist/cli.js CHANGED
File without changes
package/dist/emitter.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Generated by CoffeeScript 2.7.0
2
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;
3
+ var EmitState, VOID_ELEMENTS, attrNameToJs, buildAttributes, collectCoffeePreamble, compileComponentBody, emitArrowOutput, emitArrowOutputToJs, emitChildToJs, emitCoffeeBlock, emitCoffeeYield, emitComment, emitConditional, emitControlFlow, emitControlFlowToJs, emitDoctype, emitElement, emitElseBranch, emitFilter, emitFilterToJs, emitImplicitDiv, emitLoop, emitNode, emitNodes, emitOutput, emitStatement, emitText, escapeString, mapSourceLocation, splitJsStatements, tagToJs;
4
4
 
5
5
  import {
6
6
  Document,
@@ -11,6 +11,9 @@ import {
11
11
  ControlFlow,
12
12
  Comment,
13
13
  Filter,
14
+ CoffeeBlock,
15
+ CoffeeYield,
16
+ CoffeePreamble,
14
17
  Doctype,
15
18
  Node,
16
19
  Expression
@@ -18,7 +21,8 @@ import {
18
21
 
19
22
  import {
20
23
  compileExpression,
21
- compileStatement
24
+ compileStatement,
25
+ compileYield
22
26
  } from './expressions.js';
23
27
 
24
28
  import {
@@ -36,6 +40,11 @@ EmitState = (function() {
36
40
  class EmitState {
37
41
  constructor(options1 = {}) {
38
42
  this.options = options1;
43
+ // `hoisted` and `warnings` must be instance-level (not prototype-level):
44
+ // CoffeeScript emits class-body arrays as shared prototype properties, so
45
+ // every EmitState would otherwise mutate the same accumulator across calls.
46
+ this.hoisted = [];
47
+ this.warnings = [];
39
48
  if (this.options.sourceMap && this.options.filename) {
40
49
  this.sourceMapGenerator = new SourceMapGenerator({
41
50
  file: this.options.filename
@@ -110,7 +119,7 @@ EmitState = (function() {
110
119
 
111
120
  // ─── Public API ────────────────────────────────────────────
112
121
  export var emit = function(ast, options = {}) {
113
- var bodyExpr, hocs, inner, j, len, line, name, ref, result, state, wrap, wrapped;
122
+ var bodyExpr, hocs, inner, j, k, len, len1, line, name, p, preambles, ref, result, state, wrap, wrapped;
114
123
  state = new EmitState(options);
115
124
  state.emitLine(`import { jsx, jsxs, Fragment } from \"${options.jsxRuntime || 'react/jsx-runtime'}\";`);
116
125
  // Emit prologue lines
@@ -122,6 +131,18 @@ export var emit = function(ast, options = {}) {
122
131
  }
123
132
  }
124
133
  state.emitLine();
134
+ // Emit fenced `~~~ ... ~~~` blocks at module scope. Imports, helpers and
135
+ // setup code must live outside any component wrapper (and cannot rerun per
136
+ // render), so they are collected here and emitted before the body. This is
137
+ // the once-at-import region; `---` is the render-time counterpart.
138
+ preambles = collectCoffeePreamble(ast.children, []);
139
+ if (preambles.length > 0) {
140
+ for (k = 0, len1 = preambles.length; k < len1; k++) {
141
+ p = preambles[k];
142
+ state.emitLine(compileStatement(p.content, p.location));
143
+ }
144
+ state.emitLine();
145
+ }
125
146
  wrap = options.wrap;
126
147
  if (wrap && wrap !== 'none') {
127
148
  name = options.componentName || 'Component';
@@ -231,6 +252,34 @@ splitJsStatements = function(body) {
231
252
  };
232
253
 
233
254
  // ─── Node Emitters ─────────────────────────────────────────
255
+
256
+ // Recursively collect fenced `~~~ ... ~~~` preamble blocks, removing them
257
+ // from the render tree in place (they contribute no markup). Their statements
258
+ // are hoisted to module scope — imports, helpers, and once-only setup.
259
+ collectCoffeePreamble = function(nodes, acc) {
260
+ var i, node, ref;
261
+ if (!(nodes != null ? nodes.length : void 0)) {
262
+ return acc;
263
+ }
264
+ i = 0;
265
+ while (i < nodes.length) {
266
+ node = nodes[i];
267
+ if (node instanceof CoffeePreamble) {
268
+ acc.push(node);
269
+ nodes.splice(i, 1);
270
+ continue;
271
+ }
272
+ if (((ref = node.children) != null ? ref.length : void 0) > 0) {
273
+ collectCoffeePreamble(node.children, acc);
274
+ }
275
+ if (node.next != null) {
276
+ collectCoffeePreamble([node.next], acc);
277
+ }
278
+ i++;
279
+ }
280
+ return acc;
281
+ };
282
+
234
283
  emitNodes = function(nodes, state, isRoot = false) {
235
284
  var i, node, remaining;
236
285
  i = 0;
@@ -248,6 +297,15 @@ emitNodes = function(nodes, state, isRoot = false) {
248
297
  };
249
298
 
250
299
  emitNode = function(node, state, isRoot) {
300
+ if (node instanceof CoffeeBlock) {
301
+ return emitCoffeeBlock(node, state);
302
+ }
303
+ if (node instanceof CoffeePreamble) { // hoisted to module scope above
304
+ return;
305
+ }
306
+ if (node instanceof CoffeeYield) {
307
+ return emitCoffeeYield(node, state);
308
+ }
251
309
  if (node instanceof Element) {
252
310
  return emitElement(node, state);
253
311
  } else if (node instanceof ImplicitDiv) {
@@ -396,7 +454,7 @@ emitImplicitDiv = function(div, state) {
396
454
 
397
455
  // ─── Child Emission ────────────────────────────────────────
398
456
  emitChildToJs = function(node, state) {
399
- var allAttrs, attrResult, attrs, attrsJs, c, childParts, childrenAttr, el, exprSource, fn, hasChildren, keyArg, tagJs, textContinuations;
457
+ var allAttrs, attrResult, attrs, attrsJs, c, childParts, childrenAttr, el, exprSource, fn, hasChildren, js, keyArg, tagJs, textContinuations;
400
458
  if (node instanceof Text) {
401
459
  return escapeString(node.value);
402
460
  }
@@ -456,6 +514,13 @@ emitChildToJs = function(node, state) {
456
514
  if (node instanceof Filter) {
457
515
  return emitFilterToJs(node, state);
458
516
  }
517
+ if (node instanceof CoffeeBlock) {
518
+ js = compileStatement(node.content, node.location);
519
+ return `/* - ${js} */ null`;
520
+ }
521
+ if (node instanceof CoffeeYield) {
522
+ return compileYield(node.content, node.location);
523
+ }
459
524
  return 'null';
460
525
  };
461
526
 
@@ -496,6 +561,20 @@ emitStatement = function(cf, remaining, state) {
496
561
  }
497
562
  };
498
563
 
564
+ // Fenced `--- ... ---` block: the multiline counterpart to `-`. Compiles the
565
+ // body to statements that run at render time, producing no output. In a
566
+ // wrapped component they hoist alongside `-` statements (same IIFE path); in
567
+ // unwrapped mode they emit in place.
568
+ emitCoffeeBlock = function(node, state) {
569
+ var js;
570
+ js = compileStatement(node.content, node.location);
571
+ if (state.directEmit) {
572
+ return state.emitLine(js + ';');
573
+ } else {
574
+ return state.hoisted.push(js);
575
+ }
576
+ };
577
+
499
578
  emitLoop = function(cf, remaining, state) {
500
579
  var cond, expr, forMatch, iterable, vars;
501
580
  if (cf.controlKind === 'for') {
@@ -683,6 +762,12 @@ emitOutput = function(out, state) {
683
762
  return state.emitLine(result + ';');
684
763
  };
685
764
 
765
+ emitCoffeeYield = function(node, state) {
766
+ var js;
767
+ js = compileYield(node.content, node.location);
768
+ return state.emitLine(`jsx(Fragment, { children: ${js} });`);
769
+ };
770
+
686
771
  emitArrowOutput = function(exprSource, elementChildren, out, state) {
687
772
  var arrowArgs, arrowFn, arrowMatch, body, callTarget, compiledTarget, result, stripped;
688
773
  stripped = exprSource.replace(/\s*->\s*$/, '').trim();
@@ -1,6 +1,6 @@
1
1
  // Generated by CoffeeScript 2.7.0
2
2
  // expressions.coffee — CoffeeScript expression compilation bridge
3
- var _coffeeScriptAvailable, _coffeeScriptUnavailableReason, _require, checkCoffeeScript, collapseWs, loadCoffeeScript, objectTarget, splitTopLevelStatements, stripCoffeeWrapper, stripToConst,
3
+ var _coffeeScriptAvailable, _coffeeScriptUnavailableReason, _require, checkCoffeeScript, collapseWs, isSingleExpression, loadCoffeeScript, objectTarget, splitTopLevelStatements, stripCoffeeWrapper, stripToConst,
4
4
  indexOf = [].indexOf;
5
5
 
6
6
  import {
@@ -179,6 +179,55 @@ export var compileStatement = function(source, location = null) {
179
179
  return source;
180
180
  };
181
181
 
182
+ // ─── Compile yield block to JS expression ──────────────────
183
+ // Compiles a fenced `=== ... ===` body to a single JS expression whose
184
+ // value is the body's final result (the multiline counterpart to `=`).
185
+ // A single expression compiles cleanly; multiple statements are wrapped
186
+ // in an IIFE so CoffeeScript's implicit return becomes the yielded value.
187
+ export var compileYield = function(source, location = null) {
188
+ var CoffeeScript, e, indented, js, msg, wrapped;
189
+ if (source.trim() === '') {
190
+ return '';
191
+ }
192
+ try {
193
+ CoffeeScript = loadCoffeeScript();
194
+ if (!(CoffeeScript && typeof CoffeeScript.compile === 'function')) {
195
+ return source;
196
+ }
197
+ if (isSingleExpression(source)) {
198
+ return compileExpression(new Expression(source), location);
199
+ }
200
+ indented = source.replace(/^/gm, ' ');
201
+ wrapped = `(->\n${indented}\n)()`;
202
+ js = CoffeeScript.compile(wrapped, {
203
+ bare: true,
204
+ inlineMap: false
205
+ });
206
+ return collapseWs(js.trim()).replace(/;\s*$/, '');
207
+ } catch (error) {
208
+ e = error;
209
+ msg = e instanceof Error ? e.message : String(e);
210
+ if (location) {
211
+ throw new CompileError(`CoffeeScript yield error: ${msg}`, 'emitter', 'YIELD_ERROR', location, `Check block: ${source.slice(0, 80)}`);
212
+ }
213
+ }
214
+ return source;
215
+ };
216
+
217
+ isSingleExpression = function(source) {
218
+ var CoffeeScript, ref, ref1, root;
219
+ try {
220
+ CoffeeScript = loadCoffeeScript();
221
+ if (CoffeeScript && typeof CoffeeScript.nodes === 'function') {
222
+ root = CoffeeScript.nodes(source);
223
+ return (root != null ? (ref = root.body) != null ? (ref1 = ref.expressions) != null ? ref1.length : void 0 : void 0 : void 0) === 1;
224
+ }
225
+ } catch (error) {
226
+ false;
227
+ }
228
+ return false;
229
+ };
230
+
182
231
  // ─── Convert var → const/let ───────────────────────────────
183
232
 
184
233
  // Extract the bound variable name from one entry of an object
@@ -194,7 +243,7 @@ objectTarget = function(pair) {
194
243
  };
195
244
 
196
245
  stripToConst = function(js) {
197
- var allConsumed, assignments, body, consumed, inner, j, len, m, n, name, names, s, stmts, varIdx, varNames;
246
+ var allConsumed, body, constReplacements, consumed, idx, inner, j, len, m, n, name, names, s, stmts, varIdx, varNames;
198
247
  stmts = splitTopLevelStatements(js);
199
248
  if (stmts.length === 0) {
200
249
  return js.replace(/;\s*$/, '');
@@ -220,10 +269,10 @@ stripToConst = function(js) {
220
269
  }
221
270
  return results;
222
271
  })();
223
- assignments = [];
272
+ constReplacements = {};
224
273
  consumed = [];
225
- for (j = 0, len = body.length; j < len; j++) {
226
- s = body[j];
274
+ for (idx = j = 0, len = body.length; j < len; idx = ++j) {
275
+ s = body[idx];
227
276
  m = s.match(/^\[([^\]]*)\]\s*=/);
228
277
  if (m != null) {
229
278
  // Array destructuring: [a, b] = expr
@@ -240,7 +289,7 @@ stripToConst = function(js) {
240
289
  return results;
241
290
  })();
242
291
  consumed = consumed.concat(names);
243
- assignments.push('const ' + s);
292
+ constReplacements[idx] = 'const ' + s;
244
293
  continue;
245
294
  }
246
295
  m = s.match(/^\(\s*\{([^}]*)\}\s*=.*\)$/);
@@ -260,7 +309,7 @@ stripToConst = function(js) {
260
309
  return results;
261
310
  })();
262
311
  consumed = consumed.concat(names);
263
- assignments.push('const ' + inner);
312
+ constReplacements[idx] = 'const ' + inner;
264
313
  continue;
265
314
  }
266
315
  name = varNames.find(function(n) {
@@ -268,14 +317,24 @@ stripToConst = function(js) {
268
317
  });
269
318
  if (name != null) {
270
319
  consumed.push(name);
271
- assignments.push('const ' + s);
320
+ constReplacements[idx] = 'const ' + s;
272
321
  }
273
322
  }
274
323
  allConsumed = varNames.every(function(n) {
275
324
  return indexOf.call(consumed, n) >= 0;
276
325
  });
277
- if (assignments.length > 0 && allConsumed) {
278
- return assignments.join('; ');
326
+ if (allConsumed && Object.keys(constReplacements).length > 0) {
327
+ return ((function() {
328
+ var k, len1, ref, results;
329
+ results = [];
330
+ for (idx = k = 0, len1 = body.length; k < len1; idx = ++k) {
331
+ s = body[idx];
332
+ // Preserve non-assignment statements (imports, calls, declarations)
333
+ // in their original order, converting only bound assignments to const.
334
+ results.push((ref = constReplacements[idx]) != null ? ref : s);
335
+ }
336
+ return results;
337
+ })()).join('; ');
279
338
  }
280
339
  return ((function() {
281
340
  var k, len1, results;
package/dist/lexer.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Generated by CoffeeScript 2.7.0
2
2
  // lexer.coffee — CoffeeHaml tokenizer
3
3
  // Converts raw .chaml source into a token stream with INDENT/DEDENT
4
- var countIndent, dedentToken, extractBracketed, indentToken, isHamlConstruct, looksLikeBareAttributes, parseModifiersAndAttrs, preMergeBracketLines, tokenizeLine;
4
+ var countIndent, dedentLines, dedentToken, extractBracketed, indentToken, isHamlConstruct, looksLikeBareAttributes, parseModifiersAndAttrs, preMergeBracketLines, stripIndent, tokenizeLine;
5
5
 
6
6
  import {
7
7
  SourceLocation
@@ -21,6 +21,9 @@ export var TokenType = {
21
21
  COMMENT: 'COMMENT',
22
22
  HTML_COMMENT: 'HTML_COMMENT',
23
23
  FILTER: 'FILTER',
24
+ COFFEE_BLOCK: 'COFFEE_BLOCK',
25
+ COFFEE_YIELD: 'COFFEE_YIELD',
26
+ COFFEE_PREAMBLE: 'COFFEE_PREAMBLE',
24
27
  DOCTYPE: 'DOCTYPE',
25
28
  TEXT: 'TEXT',
26
29
  PROLOGUE: 'PROLOGUE',
@@ -75,21 +78,23 @@ preMergeBracketLines = function(lines) {
75
78
  };
76
79
 
77
80
  export var tokenize = function(source, filename = null) {
78
- var content, currentIndent, inPrologue, indent, indentStack, j, len, line, lineIndex, lineLength, lineStartOffset, lineTokens, lines, offset, rawLine, tokens;
81
+ var body, bodyLine, content, currentIndent, fence, fenceIndent, fenceStartLine, fenceStartOffset, i, inPrologue, indent, indentStack, line, lineIndex, lineLength, lineStartOffset, lineTokens, lines, offset, rawLine, tokens;
79
82
  tokens = [];
80
83
  lines = preMergeBracketLines(source.split('\n'));
81
84
  indentStack = [0];
82
85
  offset = 0;
83
86
  lineIndex = 0;
84
87
  inPrologue = true;
85
- for (j = 0, len = lines.length; j < len; j++) {
86
- rawLine = lines[j];
88
+ i = 0;
89
+ while (i < lines.length) {
90
+ rawLine = lines[i];
87
91
  line = rawLine.replace(/\r$/, '');
88
92
  lineLength = rawLine.length + 1;
89
93
  // Skip empty lines
90
94
  if (line.trim() === '') {
91
95
  offset += lineLength;
92
96
  lineIndex++;
97
+ i++;
93
98
  continue;
94
99
  }
95
100
  indent = countIndent(line);
@@ -97,6 +102,85 @@ export var tokenize = function(source, filename = null) {
97
102
  if (content === '') {
98
103
  offset += lineLength;
99
104
  lineIndex++;
105
+ i++;
106
+ continue;
107
+ }
108
+ // ─── Multiline CoffeeScript fences: --- / === / ~~~ ───
109
+ // A line consisting solely of `---`, `===` or `~~~` opens a raw
110
+ // CoffeeScript area that escapes HAML indentation. `---` compiles the
111
+ // body to render-time statements (COFFEE_BLOCK — the multiline
112
+ // counterpart to `-`); `===` yields the body's final value as content
113
+ // (COFFEE_YIELD — the multiline counterpart to `=`); `~~~` compiles the
114
+ // body to code hoisted to module scope, running once at import
115
+ // (COFFEE_PREAMBLE). The body runs until the matching closing fence line;
116
+ // it is dedented to column 0 and tokenized as a single token (no
117
+ // INDENT/DEDENT inside).
118
+ fence = content.trim();
119
+ if (fence === '---' || fence === '===' || fence === '~~~') {
120
+ inPrologue = false;
121
+ fenceIndent = indent;
122
+ fenceStartLine = lineIndex;
123
+ fenceStartOffset = offset + indent;
124
+ // Handle indentation change (same as a normal line)
125
+ currentIndent = indentStack[indentStack.length - 1];
126
+ if (fenceIndent > currentIndent) {
127
+ indentStack.push(fenceIndent);
128
+ tokens.push(indentToken(fenceIndent, fenceStartOffset, fenceIndent, filename, fenceStartLine));
129
+ } else if (fenceIndent < currentIndent) {
130
+ while (indentStack.length > 1 && fenceIndent < indentStack[indentStack.length - 1]) {
131
+ indentStack.pop();
132
+ tokens.push(dedentToken(fenceStartOffset, filename, fenceStartLine));
133
+ }
134
+ if (fenceIndent !== indentStack[indentStack.length - 1]) {
135
+ indentStack.push(fenceIndent);
136
+ tokens.push(indentToken(fenceIndent, fenceStartOffset, fenceIndent, filename, fenceStartLine));
137
+ }
138
+ }
139
+ // Consume opening fence
140
+ offset += lineLength;
141
+ lineIndex++;
142
+ i++;
143
+ // Collect body until closing fence
144
+ body = [];
145
+ while (i < lines.length) {
146
+ bodyLine = lines[i].replace(/\r$/, '');
147
+ if (bodyLine.trim() === fence) {
148
+ offset += lines[i].length + 1;
149
+ lineIndex++;
150
+ i++;
151
+ break;
152
+ }
153
+ body.push(bodyLine);
154
+ offset += lines[i].length + 1;
155
+ lineIndex++;
156
+ i++;
157
+ }
158
+ tokens.push({
159
+ type: (function() {
160
+ switch (fence) {
161
+ case '===':
162
+ return TokenType.COFFEE_YIELD;
163
+ case '~~~':
164
+ return TokenType.COFFEE_PREAMBLE;
165
+ default:
166
+ return TokenType.COFFEE_BLOCK;
167
+ }
168
+ })(),
169
+ value: (dedentLines(body)).join('\n'),
170
+ location: {
171
+ start: {
172
+ line: fenceStartLine,
173
+ column: fenceIndent
174
+ },
175
+ end: {
176
+ line: lineIndex,
177
+ column: 0
178
+ },
179
+ offset: fenceStartOffset,
180
+ length: offset - fenceStartOffset,
181
+ file: filename
182
+ }
183
+ });
100
184
  continue;
101
185
  }
102
186
  // Prologue detection: non-indented JS before first HAML construct
@@ -120,6 +204,7 @@ export var tokenize = function(source, filename = null) {
120
204
  });
121
205
  offset += lineLength;
122
206
  lineIndex++;
207
+ i++;
123
208
  continue;
124
209
  }
125
210
  inPrologue = false;
@@ -144,6 +229,7 @@ export var tokenize = function(source, filename = null) {
144
229
  tokens.push(...lineTokens);
145
230
  offset += lineLength;
146
231
  lineIndex++;
232
+ i++;
147
233
  }
148
234
  // Emit remaining DEDENT tokens at EOF
149
235
  while (indentStack.length > 1) {
@@ -535,6 +621,52 @@ countIndent = function(line) {
535
621
  return count;
536
622
  };
537
623
 
624
+ // Strip up to `amount` visual columns of leading whitespace (space=1, tab=2).
625
+ stripIndent = function(line, amount) {
626
+ var ch, i, seen;
627
+ seen = 0;
628
+ i = 0;
629
+ while (i < line.length && seen < amount) {
630
+ ch = line[i];
631
+ if (ch === ' ') {
632
+ seen += 1;
633
+ } else if (ch === '\t') {
634
+ seen += 2;
635
+ } else {
636
+ break;
637
+ }
638
+ i++;
639
+ }
640
+ return line.slice(i);
641
+ };
642
+
643
+ // Normalize a fenced block's body to column 0 by removing the minimum
644
+ // indentation found across its non-empty lines.
645
+ dedentLines = function(lines) {
646
+ var amount, ind, j, k, l, len, len1, min, results;
647
+ min = null;
648
+ for (j = 0, len = lines.length; j < len; j++) {
649
+ l = lines[j];
650
+ if (l.trim() === '') {
651
+ continue;
652
+ }
653
+ ind = countIndent(l);
654
+ if (min === null || ind < min) {
655
+ min = ind;
656
+ }
657
+ }
658
+ amount = min === null ? 0 : min;
659
+ if (amount === 0) {
660
+ return lines;
661
+ }
662
+ results = [];
663
+ for (k = 0, len1 = lines.length; k < len1; k++) {
664
+ l = lines[k];
665
+ results.push(stripIndent(l, amount));
666
+ }
667
+ return results;
668
+ };
669
+
538
670
  indentToken = function(_indent, offset, level, filename = null, line = 0) {
539
671
  return {
540
672
  type: TokenType.INDENT,
package/dist/parser.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Generated by CoffeeScript 2.7.0
2
2
  // parser.coffee — recursive descent parser
3
3
  // Converts token stream into AST
4
- var ParserState, cleanAttrName, findColon, findEquals, isElse, parseAttributeBlock, parseBareAttributes, parseBlock, parseComment, parseControlFlow, parseControlKind, parseDoctype, parseElement, parseFilter, parseHtmlComment, parseImplicitDiv, parseNode, parseOutput, parseText, splitAttributePairs, splitBareAttributePairs, stripControlKeyword;
4
+ var ParserState, cleanAttrName, findColon, findEquals, isElse, parseAttributeBlock, parseBareAttributes, parseBlock, parseCoffeeBlock, parseCoffeePreamble, parseCoffeeYield, parseComment, parseControlFlow, parseControlKind, parseDoctype, parseElement, parseFilter, parseHtmlComment, parseImplicitDiv, parseNode, parseOutput, parseText, splitAttributePairs, splitBareAttributePairs, stripControlKeyword;
5
5
 
6
6
  import {
7
7
  TokenType
@@ -16,6 +16,9 @@ import {
16
16
  ControlFlow,
17
17
  Comment,
18
18
  Filter,
19
+ CoffeeBlock,
20
+ CoffeeYield,
21
+ CoffeePreamble,
19
22
  Doctype,
20
23
  Expression,
21
24
  Attribute,
@@ -79,7 +82,7 @@ ParserState = (function() {
79
82
  var t;
80
83
  while (this.current()) {
81
84
  t = this.current().type;
82
- if (t === TokenType.TAG || t === TokenType.CLASS || t === TokenType.ID || t === TokenType.OUTPUT || t === TokenType.OUTPUT_UNESC || t === TokenType.CONTROL || t === TokenType.FILTER || t === TokenType.COMMENT || t === TokenType.HTML_COMMENT || t === TokenType.DOCTYPE || t === TokenType.INDENT || t === TokenType.DEDENT) {
85
+ if (t === TokenType.TAG || t === TokenType.CLASS || t === TokenType.ID || t === TokenType.OUTPUT || t === TokenType.OUTPUT_UNESC || t === TokenType.CONTROL || t === TokenType.FILTER || t === TokenType.COFFEE_BLOCK || t === TokenType.COFFEE_YIELD || t === TokenType.COFFEE_PREAMBLE || t === TokenType.COMMENT || t === TokenType.HTML_COMMENT || t === TokenType.DOCTYPE || t === TokenType.INDENT || t === TokenType.DEDENT) {
83
86
  return;
84
87
  }
85
88
  this.advance();
@@ -168,6 +171,12 @@ parseNode = function(state) {
168
171
  return parseHtmlComment(state);
169
172
  case TokenType.FILTER:
170
173
  return parseFilter(state);
174
+ case TokenType.COFFEE_BLOCK:
175
+ return parseCoffeeBlock(state);
176
+ case TokenType.COFFEE_YIELD:
177
+ return parseCoffeeYield(state);
178
+ case TokenType.COFFEE_PREAMBLE:
179
+ return parseCoffeePreamble(state);
171
180
  case TokenType.DOCTYPE:
172
181
  return parseDoctype(state);
173
182
  case TokenType.TEXT:
@@ -461,6 +470,34 @@ parseFilter = function(state) {
461
470
  return new Filter(filterName, content, token.location);
462
471
  };
463
472
 
473
+ // ─── CoffeeBlock ───────────────────────────────────────────
474
+ parseCoffeeBlock = function(state) {
475
+ var token;
476
+ token = state.expect(TokenType.COFFEE_BLOCK);
477
+ if (!token) {
478
+ return new CoffeeBlock('');
479
+ }
480
+ return new CoffeeBlock(token.value, token.location);
481
+ };
482
+
483
+ parseCoffeeYield = function(state) {
484
+ var token;
485
+ token = state.expect(TokenType.COFFEE_YIELD);
486
+ if (!token) {
487
+ return new CoffeeYield('');
488
+ }
489
+ return new CoffeeYield(token.value, token.location);
490
+ };
491
+
492
+ parseCoffeePreamble = function(state) {
493
+ var token;
494
+ token = state.expect(TokenType.COFFEE_PREAMBLE);
495
+ if (!token) {
496
+ return new CoffeePreamble('');
497
+ }
498
+ return new CoffeePreamble(token.value, token.location);
499
+ };
500
+
464
501
  // ─── Doctype ───────────────────────────────────────────────
465
502
  parseDoctype = function(state) {
466
503
  var token;
@@ -81,7 +81,7 @@ export var options = {
81
81
  attributeMultilineThreshold: {
82
82
  category: CAT.ATTRIBUTES,
83
83
  type: 'int',
84
- description: 'Break attributes across lines when count >= this. 0 = always multiline.'
84
+ description: 'Force attributes across lines when count >= this. 0 = never force (fold only when exceeding printWidth).'
85
85
  },
86
86
  attributeSort: {
87
87
  category: CAT.ATTRIBUTES,
@@ -218,7 +218,7 @@ export var defaultOptions = {
218
218
  inlineThreshold: -1,
219
219
  voidElementStyle: 'explicit',
220
220
  attributeStyle: 'preserve',
221
- attributeMultilineThreshold: 1,
221
+ attributeMultilineThreshold: 0,
222
222
  attributeSort: 'none',
223
223
  quoteStyle: 'preserve',
224
224
  coffeeScriptFormat: true,
@@ -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, printCoffeePreamble, 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,9 @@ import {
12
12
  ControlFlow,
13
13
  Comment,
14
14
  Filter,
15
+ CoffeeBlock,
16
+ CoffeeYield,
17
+ CoffeePreamble,
15
18
  Doctype,
16
19
  Node,
17
20
  Expression,
@@ -32,7 +35,7 @@ _require = createRequire(import.meta.url);
32
35
 
33
36
  _prettier = _require('prettier');
34
37
 
35
- ({group, indent, hardline, join} = _prettier.doc.builders);
38
+ ({group, indent, hardline, line, join} = _prettier.doc.builders);
36
39
 
37
40
  // ─── Format Options ────────────────────────────────────────
38
41
  // CoffeeHamlFormatOptions shape (runtime):
@@ -49,7 +52,7 @@ defaultOpts = {
49
52
  inlineThreshold: -1,
50
53
  voidElementStyle: 'self-closing',
51
54
  attributeStyle: 'preserve',
52
- attributeMultilineThreshold: 1,
55
+ attributeMultilineThreshold: 0,
53
56
  attributeSort: 'none',
54
57
  quoteStyle: 'preserve',
55
58
  coffeeScriptFormat: true,
@@ -225,6 +228,15 @@ printNode = function(path, o, printFn) {
225
228
  if (node instanceof Filter) {
226
229
  return printFilter(path, o, printFn);
227
230
  }
231
+ if (node instanceof CoffeeBlock) {
232
+ return printCoffeeBlock(path, o, printFn);
233
+ }
234
+ if (node instanceof CoffeeYield) {
235
+ return printCoffeeYield(path, o, printFn);
236
+ }
237
+ if (node instanceof CoffeePreamble) {
238
+ return printCoffeePreamble(path, o, printFn);
239
+ }
228
240
  if (node instanceof Doctype) {
229
241
  return printDoctype(path, o);
230
242
  }
@@ -259,7 +271,7 @@ printDoc = function(path, o, printFn) {
259
271
 
260
272
  // ─── Element ───────────────────────────────────────────────
261
273
  printElement = function(path, o, printFn) {
262
- var attrStyle, attrs, childDoc, childDocs, children, classes, header, id, isVoid, joined, node, ref, ref1, ref2, ref3, selfClose, tag;
274
+ var attrStyle, attrs, childDoc, childDocs, children, classes, header, id, isVoid, joined, node, ref, ref1, ref2, ref3, selfClose, sep, tag;
263
275
  node = path.node;
264
276
  tag = formatTag(node.tag, o);
265
277
  // Classes and ID
@@ -282,16 +294,18 @@ printElement = function(path, o, printFn) {
282
294
  } else if (node.isSelfClosing) {
283
295
  selfClose = '/';
284
296
  }
285
- header = `%${tag}${classes}${id}${attrs}${selfClose}`;
297
+ header = [`%${tag}${classes}${id}`, attrs, selfClose];
286
298
  // Children
287
299
  children = (ref = node.children) != null ? ref : [];
288
300
  if (children.length === 0) {
289
301
  return header;
290
302
  }
291
303
  // Inline child
292
- if (isSingleInlineChild(children) && o.controlFlowInline !== false) {
304
+ if (isSingleInlineChild(children)) {
293
305
  childDoc = (ref1 = (ref2 = path.map(printFn, 'children')) != null ? ref2[0] : void 0) != null ? ref1 : '';
294
- return `${header} ${childDoc}`;
306
+ // Output (= expr) carries its own separator; Text needs a space
307
+ sep = children[0] instanceof Output ? '' : ' ';
308
+ return [header, sep, childDoc];
295
309
  }
296
310
  childDocs = (ref3 = path.map(printFn, 'children')) != null ? ref3 : [];
297
311
  joined = joinChildrenWithBlanks(children, childDocs, o);
@@ -300,7 +314,7 @@ printElement = function(path, o, printFn) {
300
314
 
301
315
  // ─── ImplicitDiv ───────────────────────────────────────────
302
316
  printImplicitDiv = function(path, o, printFn) {
303
- var attrStyle, attrs, childDoc, childDocs, children, classes, header, id, joined, node, ref, ref1, ref2, ref3;
317
+ var attrStyle, attrs, childDoc, childDocs, children, classes, header, id, joined, node, ref, ref1, ref2, ref3, sep;
304
318
  node = path.node;
305
319
  classes = node.classes.map(function(c) {
306
320
  return `.${c}`;
@@ -308,14 +322,15 @@ printImplicitDiv = function(path, o, printFn) {
308
322
  id = node.id ? `#${node.id}` : '';
309
323
  attrStyle = detectStyle(node, o);
310
324
  attrs = formatAttributes(node, attrStyle, o);
311
- header = `${classes}${id}${attrs}`;
325
+ header = [`${classes}${id}`, attrs];
312
326
  children = (ref = node.children) != null ? ref : [];
313
327
  if (children.length === 0) {
314
328
  return header;
315
329
  }
316
330
  if (isSingleInlineChild(children)) {
317
331
  childDoc = (ref1 = (ref2 = path.map(printFn, 'children')) != null ? ref2[0] : void 0) != null ? ref1 : '';
318
- return `${header} ${childDoc}`;
332
+ sep = children[0] instanceof Output ? '' : ' ';
333
+ return [header, sep, childDoc];
319
334
  }
320
335
  childDocs = (ref3 = path.map(printFn, 'children')) != null ? ref3 : [];
321
336
  joined = joinChildrenWithBlanks(children, childDocs, o);
@@ -473,6 +488,38 @@ printFilter = function(path, o, _printFn) {
473
488
  }
474
489
  };
475
490
 
491
+ // ─── CoffeeBlock ───────────────────────────────────────────
492
+
493
+ // Build a fenced block doc using `hardline` so Prettier can re-indent the
494
+ // dedented body to the correct nesting level (raw `\n` strings would not).
495
+ printFence = function(fence, content) {
496
+ var k, len, parts, ref;
497
+ if (content) {
498
+ parts = [fence];
499
+ ref = content.split('\n');
500
+ for (k = 0, len = ref.length; k < len; k++) {
501
+ line = ref[k];
502
+ parts.push(hardline, line);
503
+ }
504
+ parts.push(hardline, fence);
505
+ return parts;
506
+ } else {
507
+ return [fence, hardline, fence];
508
+ }
509
+ };
510
+
511
+ printCoffeeBlock = function(path, o, _printFn) {
512
+ return printFence('---', path.node.content);
513
+ };
514
+
515
+ printCoffeeYield = function(path, o, _printFn) {
516
+ return printFence('===', path.node.content);
517
+ };
518
+
519
+ printCoffeePreamble = function(path, o, _printFn) {
520
+ return printFence('~~~', path.node.content);
521
+ };
522
+
476
523
  // ─── Doctype ───────────────────────────────────────────────
477
524
  printDoctype = function(path, o) {
478
525
  var node;
@@ -501,12 +548,14 @@ detectStyle = function(node, o) {
501
548
  };
502
549
 
503
550
  formatAttributes = function(node, style, o) {
504
- var attr, attrs, bareParts, expr, k, len, name, parts, ref, ref1, ref2, val;
551
+ var attr, attrs, bareParts, close, expr, forceBreak, k, len, name, open, parts, ref, ref1, ref2, sep, val;
505
552
  attrs = (ref = node.attributes) != null ? ref : [];
506
553
  if (!(attrs.length > 0)) {
507
554
  return '';
508
555
  }
509
- // Bare style: HTML-style key="val" key2={expr} — space-separated
556
+ // Bare style: HTML-style key="val" key2={expr} — space-separated.
557
+ // Never folded: an indented continuation line would parse as a child
558
+ // element, silently changing the tree. Bare attributes stay inline.
510
559
  if (style === 'bare') {
511
560
  bareParts = (function() {
512
561
  var k, len, ref1, ref2, results;
@@ -528,38 +577,44 @@ formatAttributes = function(node, style, o) {
528
577
  return results;
529
578
  })();
530
579
  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 : '';
580
+ }
581
+ parts = [];
582
+ for (k = 0, len = attrs.length; k < len; k++) {
583
+ attr = attrs[k];
584
+ if (attr.spread) {
585
+ expr = (ref1 = attr.expression.source) != null ? ref1 : '';
545
586
  if (o.coffeeScriptFormat) {
546
- val = formatCoffeeScript(val, o);
547
- }
548
- if (attr.shorthand) {
549
- parts.push(name);
550
- } else {
551
- parts.push(`${name}: ${val}`);
587
+ expr = formatCoffeeScript(expr, o);
552
588
  }
589
+ parts.push(style === 'parens' ? `...${expr}` : `{${expr}...}`);
590
+ continue;
553
591
  }
554
- switch (style) {
555
- case 'braces':
556
- return `{ ${parts.join(', ')} }`;
557
- case 'parens':
558
- return `( ${parts.join(', ')} )`;
559
- default:
560
- return parts.join(', ');
592
+ name = attr.name;
593
+ val = (ref2 = attr.value.source) != null ? ref2 : '';
594
+ if (o.coffeeScriptFormat) {
595
+ val = formatCoffeeScript(val, o);
561
596
  }
597
+ if (attr.shorthand) {
598
+ parts.push(name);
599
+ } else {
600
+ parts.push(`${name}: ${val}`);
601
+ }
602
+ }
603
+ // Count-based forcing: a positive threshold breaks even when the
604
+ // attributes would otherwise fit within printWidth.
605
+ forceBreak = o.attributeMultilineThreshold > 0 && attrs.length >= o.attributeMultilineThreshold;
606
+ sep = forceBreak ? hardline : line;
607
+ if (style === 'braces') {
608
+ open = '{';
609
+ close = '}';
610
+ } else if (style === 'parens') {
611
+ open = '(';
612
+ close = ')';
613
+ } else {
614
+ // Unknown style — inline fallback, no folding.
615
+ return parts.join(', ');
562
616
  }
617
+ return group([open, indent([sep, join([',', sep], parts)]), sep, close]);
563
618
  };
564
619
 
565
620
  // ─── CoffeeScript Embed (for Prettier embed) ───────────────
package/docs/grammar.md CHANGED
@@ -292,6 +292,103 @@ 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
+ The `---` fence is the multiline counterpart to `-` (statement). The body
306
+ between the two `---` lines is taken verbatim (dedented by its minimum
307
+ indentation) and compiled to executable JavaScript via the CoffeeScript
308
+ bridge. The resulting statements run at **render time**, produce no markup,
309
+ and behave exactly like a block of `-` lines.
310
+
311
+ ```haml
312
+ ---
313
+ total = price * quantity
314
+ tax = total * 0.08
315
+ ---
316
+ %p= total + tax
317
+ ```
318
+
319
+ Semantics:
320
+
321
+ - **Escapes indentation**: body lines are dedented to column 0, so a block
322
+ nested under an element still writes CoffeeScript flush-left.
323
+ - **Render-time**: the compiled statements run on every render, before the
324
+ JSX at their position. In a wrapped component they are hoisted into the
325
+ render expression alongside `-` statements; with `wrap: 'none'` they emit
326
+ in place.
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 running as statements 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
+
361
+ ## Preamble Blocks (`~~~`)
362
+
363
+ ```
364
+ CoffeePreamble := '~~~' Newline
365
+ CoffeeBody
366
+ '~~~' Newline
367
+ ```
368
+
369
+ The `~~~` fence holds module-scope code. Its body is CoffeeScript, dedented
370
+ to column 0, and compiled to statements hoisted to **module scope** — outside
371
+ any component wrapper. This is where imports, helper functions and one-time
372
+ setup code live (the once-at-import region).
373
+
374
+ ```haml
375
+ ~~~
376
+ import { useState } from 'react'
377
+ formatPrice = (n) -> "$#{n.toFixed 2}"
378
+ ~~~
379
+ %p= formatPrice total
380
+ ```
381
+
382
+ Semantics:
383
+
384
+ - **Module scope**: the compiled statements run once at module load, before
385
+ any JSX. `import`/`export` stay valid (they are not wrapped in a function).
386
+ - **No markup**: the block contributes nothing to the render tree.
387
+ - **Runs once**: unlike `---` (render-time), the body does not rerun per
388
+ render — ideal for imports, constants and pure helpers.
389
+
390
+ ---
391
+
295
392
  ## Raw Text / Passthrough
296
393
 
297
394
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coffeehaml",
3
- "version": "0.7.7",
3
+ "version": "0.10.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/run-compiler-tests.mjs && 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
  },
@@ -15,6 +15,18 @@
15
15
  <key>include</key>
16
16
  <string>#prologue</string>
17
17
  </dict>
18
+ <dict>
19
+ <key>include</key>
20
+ <string>#coffee-block</string>
21
+ </dict>
22
+ <dict>
23
+ <key>include</key>
24
+ <string>#coffee-yield</string>
25
+ </dict>
26
+ <dict>
27
+ <key>include</key>
28
+ <string>#coffee-preamble</string>
29
+ </dict>
18
30
  <dict>
19
31
  <key>include</key>
20
32
  <string>#comment-haml</string>
@@ -183,6 +195,40 @@
183
195
  <key>name</key>
184
196
  <string>meta.attribute.shorthand.coffeehaml</string>
185
197
  </dict>
198
+ <key>coffee-block</key>
199
+ <dict>
200
+ <key>begin</key>
201
+ <string>^([ \t]*)(---)[ \t]*$</string>
202
+ <key>beginCaptures</key>
203
+ <dict>
204
+ <key>2</key>
205
+ <dict>
206
+ <key>name</key>
207
+ <string>punctuation.definition.coffee-block.coffeehaml</string>
208
+ </dict>
209
+ </dict>
210
+ <key>contentName</key>
211
+ <string>source.coffee.embedded.haml</string>
212
+ <key>end</key>
213
+ <string>^([ \t]*)(---)[ \t]*$</string>
214
+ <key>endCaptures</key>
215
+ <dict>
216
+ <key>2</key>
217
+ <dict>
218
+ <key>name</key>
219
+ <string>punctuation.definition.coffee-block.coffeehaml</string>
220
+ </dict>
221
+ </dict>
222
+ <key>name</key>
223
+ <string>meta.coffee-block.coffeehaml</string>
224
+ <key>patterns</key>
225
+ <array>
226
+ <dict>
227
+ <key>include</key>
228
+ <string>source.coffee</string>
229
+ </dict>
230
+ </array>
231
+ </dict>
186
232
  <key>coffee-boolean</key>
187
233
  <dict>
188
234
  <key>match</key>
@@ -566,6 +612,74 @@
566
612
  <key>name</key>
567
613
  <string>variable.other.coffeehaml</string>
568
614
  </dict>
615
+ <key>coffee-yield</key>
616
+ <dict>
617
+ <key>begin</key>
618
+ <string>^([ \t]*)(===)[ \t]*$</string>
619
+ <key>beginCaptures</key>
620
+ <dict>
621
+ <key>2</key>
622
+ <dict>
623
+ <key>name</key>
624
+ <string>punctuation.definition.coffee-yield.coffeehaml</string>
625
+ </dict>
626
+ </dict>
627
+ <key>contentName</key>
628
+ <string>source.coffee.embedded.haml</string>
629
+ <key>end</key>
630
+ <string>^([ \t]*)(===)[ \t]*$</string>
631
+ <key>endCaptures</key>
632
+ <dict>
633
+ <key>2</key>
634
+ <dict>
635
+ <key>name</key>
636
+ <string>punctuation.definition.coffee-yield.coffeehaml</string>
637
+ </dict>
638
+ </dict>
639
+ <key>name</key>
640
+ <string>meta.coffee-yield.coffeehaml</string>
641
+ <key>patterns</key>
642
+ <array>
643
+ <dict>
644
+ <key>include</key>
645
+ <string>source.coffee</string>
646
+ </dict>
647
+ </array>
648
+ </dict>
649
+ <key>coffee-preamble</key>
650
+ <dict>
651
+ <key>begin</key>
652
+ <string>^([ \t]*)(~~~)[ \t]*$</string>
653
+ <key>beginCaptures</key>
654
+ <dict>
655
+ <key>2</key>
656
+ <dict>
657
+ <key>name</key>
658
+ <string>punctuation.definition.coffee-preamble.coffeehaml</string>
659
+ </dict>
660
+ </dict>
661
+ <key>contentName</key>
662
+ <string>source.coffee.embedded.haml</string>
663
+ <key>end</key>
664
+ <string>^([ \t]*)(~~~)[ \t]*$</string>
665
+ <key>endCaptures</key>
666
+ <dict>
667
+ <key>2</key>
668
+ <dict>
669
+ <key>name</key>
670
+ <string>punctuation.definition.coffee-preamble.coffeehaml</string>
671
+ </dict>
672
+ </dict>
673
+ <key>name</key>
674
+ <string>meta.coffee-preamble.coffeehaml</string>
675
+ <key>patterns</key>
676
+ <array>
677
+ <dict>
678
+ <key>include</key>
679
+ <string>source.coffee</string>
680
+ </dict>
681
+ </array>
682
+ </dict>
569
683
  <key>coffeescript-expression</key>
570
684
  <dict>
571
685
  <key>patterns</key>
@@ -971,4 +1085,4 @@
971
1085
  <key>uuid</key>
972
1086
  <string>1EEA33FD-AEAC-4C44-9EB6-D4D0D9E5C9CB</string>
973
1087
  </dict>
974
- </plist>
1088
+ </plist>
@@ -11,6 +11,15 @@
11
11
  {
12
12
  "include": "#prologue"
13
13
  },
14
+ {
15
+ "include": "#coffee-block"
16
+ },
17
+ {
18
+ "include": "#coffee-yield"
19
+ },
20
+ {
21
+ "include": "#coffee-preamble"
22
+ },
14
23
  {
15
24
  "include": "#comment-haml"
16
25
  },
@@ -300,6 +309,69 @@
300
309
  ],
301
310
  "contentName": "source.coffee.embedded.haml"
302
311
  },
312
+ "coffee-block": {
313
+ "name": "meta.coffee-block.coffeehaml",
314
+ "begin": "^([ \\t]*)(---)[ \\t]*$",
315
+ "beginCaptures": {
316
+ "2": {
317
+ "name": "punctuation.definition.coffee-block.coffeehaml"
318
+ }
319
+ },
320
+ "end": "^([ \\t]*)(---)[ \\t]*$",
321
+ "endCaptures": {
322
+ "2": {
323
+ "name": "punctuation.definition.coffee-block.coffeehaml"
324
+ }
325
+ },
326
+ "patterns": [
327
+ {
328
+ "include": "source.coffee"
329
+ }
330
+ ],
331
+ "contentName": "source.coffee.embedded.haml"
332
+ },
333
+ "coffee-yield": {
334
+ "name": "meta.coffee-yield.coffeehaml",
335
+ "begin": "^([ \\t]*)(===)[ \\t]*$",
336
+ "beginCaptures": {
337
+ "2": {
338
+ "name": "punctuation.definition.coffee-yield.coffeehaml"
339
+ }
340
+ },
341
+ "end": "^([ \\t]*)(===)[ \\t]*$",
342
+ "endCaptures": {
343
+ "2": {
344
+ "name": "punctuation.definition.coffee-yield.coffeehaml"
345
+ }
346
+ },
347
+ "patterns": [
348
+ {
349
+ "include": "source.coffee"
350
+ }
351
+ ],
352
+ "contentName": "source.coffee.embedded.haml"
353
+ },
354
+ "coffee-preamble": {
355
+ "name": "meta.coffee-preamble.coffeehaml",
356
+ "begin": "^([ \\t]*)(~~~)[ \\t]*$",
357
+ "beginCaptures": {
358
+ "2": {
359
+ "name": "punctuation.definition.coffee-preamble.coffeehaml"
360
+ }
361
+ },
362
+ "end": "^([ \\t]*)(~~~)[ \\t]*$",
363
+ "endCaptures": {
364
+ "2": {
365
+ "name": "punctuation.definition.coffee-preamble.coffeehaml"
366
+ }
367
+ },
368
+ "patterns": [
369
+ {
370
+ "include": "source.coffee"
371
+ }
372
+ ],
373
+ "contentName": "source.coffee.embedded.haml"
374
+ },
303
375
  "output": {
304
376
  "name": "meta.output.coffeehaml",
305
377
  "begin": "^(\\s*)(!=|=)(?!=)(\\s*)",