coffeehaml 0.8.0 → 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,8 +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 — module-scope code (imports, helpers, setup) |
155
+ | `---` … `---` | Fenced CoffeeScript block — render-time statements (multiline `-`) |
156
156
  | `===` … `===` | Fenced yield block — CoffeeScript whose final value renders (escaped) |
157
+ | `~~~` … `~~~` | Fenced preamble block — module-scope code (imports, helpers, setup) |
157
158
  | `-# comment` | Haml comment (stripped) |
158
159
  | `/ comment` | HTML comment |
159
160
  | `.wrapper` | Implicit div |
@@ -175,7 +176,7 @@ Full grammar: [`docs/grammar.md`](docs/grammar.md)
175
176
 
176
177
  ## Status
177
178
 
178
- **v0.8.0 — production beta.** The compiler pipeline (Lexer → Parser → Emitter)
179
+ **v0.10.0 — production beta.** The compiler pipeline (Lexer → Parser → Emitter)
179
180
  is complete. The [Prettier plugin](src/prettier/) provides 16 deactivatable
180
181
  formatting options including attribute style preservation, statement merging,
181
182
  blank line handling, and CoffeeScript code formatting.
@@ -196,8 +197,9 @@ blank line handling, and CoffeeScript code formatting.
196
197
  | Haml/HTML comments | ✅ |
197
198
  | `:filter` blocks | ✅ |
198
199
  | Prologue passthrough (`import`, `@decorator`) | ✅ |
199
- | `---` module-scope CoffeeScript block | ✅ |
200
+ | `---` render-time CoffeeScript block | ✅ |
200
201
  | `===` yield block (fenced output) | ✅ |
202
+ | `~~~` module-scope preamble block | ✅ |
201
203
  | Component / HOC wrapping (`wrap: 'observer'`) | ✅ |
202
204
  | Vite plugin with HMR | ✅ |
203
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', 'CoffeeBlock', 'CoffeeYield', '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
 
@@ -322,8 +322,9 @@ export var Filter = (function() {
322
322
  export var CoffeeBlock = (function() {
323
323
  // ─── CoffeeBlock ───────────────────────────────────────────
324
324
  // A fenced raw CoffeeScript block (--- ... ---). Its content is a
325
- // multi-line CoffeeScript program, compiled to executable JS statements
326
- // and hoisted to module scope (imports, helpers, setup code).
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.
327
328
  class CoffeeBlock extends Node {
328
329
  constructor(content, location = null) {
329
330
  super(location);
@@ -340,6 +341,28 @@ export var CoffeeBlock = (function() {
340
341
 
341
342
  }).call(this);
342
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
+
343
366
  export var CoffeeYield = (function() {
344
367
  // ─── CoffeeYield ───────────────────────────────────────────
345
368
  // A fenced CoffeeScript yield block (=== ... ===). The body is a
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, collectCoffeeBlocks, compileComponentBody, emitArrowOutput, emitArrowOutputToJs, emitChildToJs, emitCoffeeYield, 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,
@@ -13,6 +13,7 @@ import {
13
13
  Filter,
14
14
  CoffeeBlock,
15
15
  CoffeeYield,
16
+ CoffeePreamble,
16
17
  Doctype,
17
18
  Node,
18
19
  Expression
@@ -39,6 +40,11 @@ EmitState = (function() {
39
40
  class EmitState {
40
41
  constructor(options1 = {}) {
41
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 = [];
42
48
  if (this.options.sourceMap && this.options.filename) {
43
49
  this.sourceMapGenerator = new SourceMapGenerator({
44
50
  file: this.options.filename
@@ -113,7 +119,7 @@ EmitState = (function() {
113
119
 
114
120
  // ─── Public API ────────────────────────────────────────────
115
121
  export var emit = function(ast, options = {}) {
116
- var b, blocks, bodyExpr, hocs, inner, j, k, len, len1, 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;
117
123
  state = new EmitState(options);
118
124
  state.emitLine(`import { jsx, jsxs, Fragment } from \"${options.jsxRuntime || 'react/jsx-runtime'}\";`);
119
125
  // Emit prologue lines
@@ -125,14 +131,15 @@ export var emit = function(ast, options = {}) {
125
131
  }
126
132
  }
127
133
  state.emitLine();
128
- // Emit fenced CoffeeScript blocks (--- ... ---) at module scope.
129
- // Imports, helpers and setup code must live outside any component
130
- // wrapper, so they are collected here and emitted before the body.
131
- blocks = collectCoffeeBlocks(ast.children, []);
132
- if (blocks.length > 0) {
133
- for (k = 0, len1 = blocks.length; k < len1; k++) {
134
- b = blocks[k];
135
- state.emitLine(compileStatement(b.content, b.location));
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));
136
143
  }
137
144
  state.emitLine();
138
145
  }
@@ -246,9 +253,10 @@ splitJsStatements = function(body) {
246
253
 
247
254
  // ─── Node Emitters ─────────────────────────────────────────
248
255
 
249
- // Recursively collect fenced CoffeeScript blocks for module-level hoisting,
250
- // removing them from the render tree in place (they contribute no markup).
251
- collectCoffeeBlocks = function(nodes, acc) {
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) {
252
260
  var i, node, ref;
253
261
  if (!(nodes != null ? nodes.length : void 0)) {
254
262
  return acc;
@@ -256,16 +264,16 @@ collectCoffeeBlocks = function(nodes, acc) {
256
264
  i = 0;
257
265
  while (i < nodes.length) {
258
266
  node = nodes[i];
259
- if (node instanceof CoffeeBlock) {
267
+ if (node instanceof CoffeePreamble) {
260
268
  acc.push(node);
261
269
  nodes.splice(i, 1);
262
270
  continue;
263
271
  }
264
272
  if (((ref = node.children) != null ? ref.length : void 0) > 0) {
265
- collectCoffeeBlocks(node.children, acc);
273
+ collectCoffeePreamble(node.children, acc);
266
274
  }
267
275
  if (node.next != null) {
268
- collectCoffeeBlocks([node.next], acc);
276
+ collectCoffeePreamble([node.next], acc);
269
277
  }
270
278
  i++;
271
279
  }
@@ -289,7 +297,10 @@ emitNodes = function(nodes, state, isRoot = false) {
289
297
  };
290
298
 
291
299
  emitNode = function(node, state, isRoot) {
292
- if (node instanceof CoffeeBlock) { // hoisted to module scope above
300
+ if (node instanceof CoffeeBlock) {
301
+ return emitCoffeeBlock(node, state);
302
+ }
303
+ if (node instanceof CoffeePreamble) { // hoisted to module scope above
293
304
  return;
294
305
  }
295
306
  if (node instanceof CoffeeYield) {
@@ -443,7 +454,7 @@ emitImplicitDiv = function(div, state) {
443
454
 
444
455
  // ─── Child Emission ────────────────────────────────────────
445
456
  emitChildToJs = function(node, state) {
446
- 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;
447
458
  if (node instanceof Text) {
448
459
  return escapeString(node.value);
449
460
  }
@@ -503,6 +514,10 @@ emitChildToJs = function(node, state) {
503
514
  if (node instanceof Filter) {
504
515
  return emitFilterToJs(node, state);
505
516
  }
517
+ if (node instanceof CoffeeBlock) {
518
+ js = compileStatement(node.content, node.location);
519
+ return `/* - ${js} */ null`;
520
+ }
506
521
  if (node instanceof CoffeeYield) {
507
522
  return compileYield(node.content, node.location);
508
523
  }
@@ -546,6 +561,20 @@ emitStatement = function(cf, remaining, state) {
546
561
  }
547
562
  };
548
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
+
549
578
  emitLoop = function(cf, remaining, state) {
550
579
  var cond, expr, forMatch, iterable, vars;
551
580
  if (cf.controlKind === 'for') {
package/dist/lexer.js CHANGED
@@ -23,6 +23,7 @@ export var TokenType = {
23
23
  FILTER: 'FILTER',
24
24
  COFFEE_BLOCK: 'COFFEE_BLOCK',
25
25
  COFFEE_YIELD: 'COFFEE_YIELD',
26
+ COFFEE_PREAMBLE: 'COFFEE_PREAMBLE',
26
27
  DOCTYPE: 'DOCTYPE',
27
28
  TEXT: 'TEXT',
28
29
  PROLOGUE: 'PROLOGUE',
@@ -104,15 +105,18 @@ export var tokenize = function(source, filename = null) {
104
105
  i++;
105
106
  continue;
106
107
  }
107
- // ─── Multiline CoffeeScript fences: --- ... --- / === ... === ───
108
- // A line consisting solely of `---` or `===` opens a raw CoffeeScript
109
- // area that escapes HAML indentation. `---` compiles the body to code
110
- // hoisted to module scope (COFFEE_BLOCK); `===` yields the body's final
111
- // value as content at that position (COFFEE_YIELD). The body runs until
112
- // the matching closing fence line; it is dedented to column 0 and
113
- // tokenized as a single token (no INDENT/DEDENT inside).
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).
114
118
  fence = content.trim();
115
- if (fence === '---' || fence === '===') {
119
+ if (fence === '---' || fence === '===' || fence === '~~~') {
116
120
  inPrologue = false;
117
121
  fenceIndent = indent;
118
122
  fenceStartLine = lineIndex;
@@ -152,7 +156,16 @@ export var tokenize = function(source, filename = null) {
152
156
  i++;
153
157
  }
154
158
  tokens.push({
155
- type: (fence === '===' ? TokenType.COFFEE_YIELD : TokenType.COFFEE_BLOCK),
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
+ })(),
156
169
  value: (dedentLines(body)).join('\n'),
157
170
  location: {
158
171
  start: {
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, parseCoffeeBlock, parseCoffeeYield, 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
@@ -18,6 +18,7 @@ import {
18
18
  Filter,
19
19
  CoffeeBlock,
20
20
  CoffeeYield,
21
+ CoffeePreamble,
21
22
  Doctype,
22
23
  Expression,
23
24
  Attribute,
@@ -81,7 +82,7 @@ ParserState = (function() {
81
82
  var t;
82
83
  while (this.current()) {
83
84
  t = this.current().type;
84
- 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.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) {
85
86
  return;
86
87
  }
87
88
  this.advance();
@@ -174,6 +175,8 @@ parseNode = function(state) {
174
175
  return parseCoffeeBlock(state);
175
176
  case TokenType.COFFEE_YIELD:
176
177
  return parseCoffeeYield(state);
178
+ case TokenType.COFFEE_PREAMBLE:
179
+ return parseCoffeePreamble(state);
177
180
  case TokenType.DOCTYPE:
178
181
  return parseDoctype(state);
179
182
  case TokenType.TEXT:
@@ -486,6 +489,15 @@ parseCoffeeYield = function(state) {
486
489
  return new CoffeeYield(token.value, token.location);
487
490
  };
488
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
+
489
501
  // ─── Doctype ───────────────────────────────────────────────
490
502
  parseDoctype = function(state) {
491
503
  var token;
@@ -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, line, mergeAllStatements, opts, printCoffeeBlock, printCoffeeYield, printComment, printControlFlow, printDoc, printDoctype, printElement, printFence, 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,
@@ -14,6 +14,7 @@ import {
14
14
  Filter,
15
15
  CoffeeBlock,
16
16
  CoffeeYield,
17
+ CoffeePreamble,
17
18
  Doctype,
18
19
  Node,
19
20
  Expression,
@@ -233,6 +234,9 @@ printNode = function(path, o, printFn) {
233
234
  if (node instanceof CoffeeYield) {
234
235
  return printCoffeeYield(path, o, printFn);
235
236
  }
237
+ if (node instanceof CoffeePreamble) {
238
+ return printCoffeePreamble(path, o, printFn);
239
+ }
236
240
  if (node instanceof Doctype) {
237
241
  return printDoctype(path, o);
238
242
  }
@@ -512,6 +516,10 @@ printCoffeeYield = function(path, o, _printFn) {
512
516
  return printFence('===', path.node.content);
513
517
  };
514
518
 
519
+ printCoffeePreamble = function(path, o, _printFn) {
520
+ return printFence('~~~', path.node.content);
521
+ };
522
+
515
523
  // ─── Doctype ───────────────────────────────────────────────
516
524
  printDoctype = function(path, o) {
517
525
  var node;
package/docs/grammar.md CHANGED
@@ -302,28 +302,28 @@ CoffeeBlock := '---' Newline
302
302
  CoffeeBody := Raw CoffeeScript lines, dedented to column 0.
303
303
  ```
304
304
 
305
- A fenced raw CoffeeScript area. The body between the two `---` lines is
306
- taken verbatim (dedented by its minimum indentation) and compiled to
307
- executable JavaScript via the CoffeeScript bridge, then hoisted to module
308
- scope — outside any component wrapper. This is how imports, helper
309
- functions and one-time setup code are expressed without fighting HAML's
310
- indentation tree.
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.
311
310
 
312
311
  ```haml
313
312
  ---
314
- import { blah } from 'ok'
315
- answer = 42
313
+ total = price * quantity
314
+ tax = total * 0.08
316
315
  ---
317
- %p= answer
316
+ %p= total + tax
318
317
  ```
319
318
 
320
319
  Semantics:
321
320
 
322
321
  - **Escapes indentation**: body lines are dedented to column 0, so a block
323
322
  nested under an element still writes CoffeeScript flush-left.
324
- - **Module scope**: the compiled statements run once at module load, before
325
- any JSX. `import`/`export` therefore stay valid (they are not wrapped in
326
- a component function).
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
327
  - **No markup**: the block contributes nothing to the render tree.
328
328
 
329
329
  Note: `---` is a CoffeeHaml fence marker, not an element. For a horizontal
@@ -342,7 +342,7 @@ CoffeeYield := '===' Newline
342
342
 
343
343
  The `===` fence is the multiline counterpart to `=` (output): its body is
344
344
  CoffeeScript, dedented to column 0 exactly like a `---` block, but instead
345
- of being hoisted to module scope its **final value is yielded as content**
345
+ of running as statements its **final value is yielded as content**
346
346
  at that position in the render tree (escaped, matching `=`).
347
347
 
348
348
  ```haml
@@ -358,6 +358,37 @@ wrapped in an IIFE so the last expression is the yielded value.
358
358
 
359
359
  ---
360
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
+
361
392
  ## Raw Text / Passthrough
362
393
 
363
394
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coffeehaml",
3
- "version": "0.8.0",
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",
@@ -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*)",