coffeehaml 0.7.6 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -152,6 +152,8 @@ 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) |
156
+ | `===` … `===` | Fenced yield block — CoffeeScript whose final value renders (escaped) |
155
157
  | `-# comment` | Haml comment (stripped) |
156
158
  | `/ comment` | HTML comment |
157
159
  | `.wrapper` | Implicit div |
@@ -173,7 +175,7 @@ Full grammar: [`docs/grammar.md`](docs/grammar.md)
173
175
 
174
176
  ## Status
175
177
 
176
- **v0.7.0 — production beta.** The compiler pipeline (Lexer → Parser → Emitter)
178
+ **v0.8.0 — production beta.** The compiler pipeline (Lexer → Parser → Emitter)
177
179
  is complete. The [Prettier plugin](src/prettier/) provides 16 deactivatable
178
180
  formatting options including attribute style preservation, statement merging,
179
181
  blank line handling, and CoffeeScript code formatting.
@@ -194,6 +196,8 @@ blank line handling, and CoffeeScript code formatting.
194
196
  | Haml/HTML comments | ✅ |
195
197
  | `:filter` blocks | ✅ |
196
198
  | Prologue passthrough (`import`, `@decorator`) | ✅ |
199
+ | `---` module-scope CoffeeScript block | ✅ |
200
+ | `===` yield block (fenced output) | ✅ |
197
201
  | Component / HOC wrapping (`wrap: 'observer'`) | ✅ |
198
202
  | Vite plugin with HMR | ✅ |
199
203
  | 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', 'Doctype'];
10
10
 
11
11
  export var ControlFlowKind = ['if', 'unless', 'for', 'while', 'else', 'statement'];
12
12
 
@@ -319,6 +319,48 @@ 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 executable JS statements
326
+ // and hoisted to module scope (imports, helpers, setup code).
327
+ class CoffeeBlock extends Node {
328
+ constructor(content, location = null) {
329
+ super(location);
330
+ this.content = content;
331
+ }
332
+
333
+ };
334
+
335
+ CoffeeBlock.prototype.kind = 'CoffeeBlock';
336
+
337
+ CoffeeBlock.prototype.content = '';
338
+
339
+ return CoffeeBlock;
340
+
341
+ }).call(this);
342
+
343
+ export var CoffeeYield = (function() {
344
+ // ─── CoffeeYield ───────────────────────────────────────────
345
+ // A fenced CoffeeScript yield block (=== ... ===). The body is a
346
+ // multi-line CoffeeScript program whose final value is yielded as
347
+ // content at that position (the multiline counterpart to `=`).
348
+ class CoffeeYield extends Node {
349
+ constructor(content, location = null) {
350
+ super(location);
351
+ this.content = content;
352
+ }
353
+
354
+ };
355
+
356
+ CoffeeYield.prototype.kind = 'CoffeeYield';
357
+
358
+ CoffeeYield.prototype.content = '';
359
+
360
+ return CoffeeYield;
361
+
362
+ }).call(this);
363
+
322
364
  export var Doctype = (function() {
323
365
  // ─── Doctype ───────────────────────────────────────────────
324
366
  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, 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;
4
4
 
5
5
  import {
6
6
  Document,
@@ -11,6 +11,8 @@ import {
11
11
  ControlFlow,
12
12
  Comment,
13
13
  Filter,
14
+ CoffeeBlock,
15
+ CoffeeYield,
14
16
  Doctype,
15
17
  Node,
16
18
  Expression
@@ -18,7 +20,8 @@ import {
18
20
 
19
21
  import {
20
22
  compileExpression,
21
- compileStatement
23
+ compileStatement,
24
+ compileYield
22
25
  } from './expressions.js';
23
26
 
24
27
  import {
@@ -110,7 +113,7 @@ EmitState = (function() {
110
113
 
111
114
  // ─── Public API ────────────────────────────────────────────
112
115
  export var emit = function(ast, options = {}) {
113
- var bodyExpr, hocs, inner, j, len, line, name, ref, result, state, wrap, wrapped;
116
+ var b, blocks, bodyExpr, hocs, inner, j, k, len, len1, line, name, ref, result, state, wrap, wrapped;
114
117
  state = new EmitState(options);
115
118
  state.emitLine(`import { jsx, jsxs, Fragment } from \"${options.jsxRuntime || 'react/jsx-runtime'}\";`);
116
119
  // Emit prologue lines
@@ -122,6 +125,17 @@ export var emit = function(ast, options = {}) {
122
125
  }
123
126
  }
124
127
  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));
136
+ }
137
+ state.emitLine();
138
+ }
125
139
  wrap = options.wrap;
126
140
  if (wrap && wrap !== 'none') {
127
141
  name = options.componentName || 'Component';
@@ -231,6 +245,33 @@ splitJsStatements = function(body) {
231
245
  };
232
246
 
233
247
  // ─── Node Emitters ─────────────────────────────────────────
248
+
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) {
252
+ var i, node, ref;
253
+ if (!(nodes != null ? nodes.length : void 0)) {
254
+ return acc;
255
+ }
256
+ i = 0;
257
+ while (i < nodes.length) {
258
+ node = nodes[i];
259
+ if (node instanceof CoffeeBlock) {
260
+ acc.push(node);
261
+ nodes.splice(i, 1);
262
+ continue;
263
+ }
264
+ if (((ref = node.children) != null ? ref.length : void 0) > 0) {
265
+ collectCoffeeBlocks(node.children, acc);
266
+ }
267
+ if (node.next != null) {
268
+ collectCoffeeBlocks([node.next], acc);
269
+ }
270
+ i++;
271
+ }
272
+ return acc;
273
+ };
274
+
234
275
  emitNodes = function(nodes, state, isRoot = false) {
235
276
  var i, node, remaining;
236
277
  i = 0;
@@ -248,6 +289,12 @@ emitNodes = function(nodes, state, isRoot = false) {
248
289
  };
249
290
 
250
291
  emitNode = function(node, state, isRoot) {
292
+ if (node instanceof CoffeeBlock) { // hoisted to module scope above
293
+ return;
294
+ }
295
+ if (node instanceof CoffeeYield) {
296
+ return emitCoffeeYield(node, state);
297
+ }
251
298
  if (node instanceof Element) {
252
299
  return emitElement(node, state);
253
300
  } else if (node instanceof ImplicitDiv) {
@@ -456,6 +503,9 @@ emitChildToJs = function(node, state) {
456
503
  if (node instanceof Filter) {
457
504
  return emitFilterToJs(node, state);
458
505
  }
506
+ if (node instanceof CoffeeYield) {
507
+ return compileYield(node.content, node.location);
508
+ }
459
509
  return 'null';
460
510
  };
461
511
 
@@ -683,6 +733,12 @@ emitOutput = function(out, state) {
683
733
  return state.emitLine(result + ';');
684
734
  };
685
735
 
736
+ emitCoffeeYield = function(node, state) {
737
+ var js;
738
+ js = compileYield(node.content, node.location);
739
+ return state.emitLine(`jsx(Fragment, { children: ${js} });`);
740
+ };
741
+
686
742
  emitArrowOutput = function(exprSource, elementChildren, out, state) {
687
743
  var arrowArgs, arrowFn, arrowMatch, body, callTarget, compiledTarget, result, stripped;
688
744
  stripped = exprSource.replace(/\s*->\s*$/, '').trim();
@@ -1,6 +1,7 @@
1
1
  // Generated by CoffeeScript 2.7.0
2
- // expressions.coffee — CoffeeScript expression compilation bridge
3
- var _coffeeScriptAvailable, _coffeeScriptUnavailableReason, _require, checkCoffeeScript, collapseWs, loadCoffeeScript, splitTopLevelStatements, stripCoffeeWrapper, stripToConst;
2
+ // expressions.coffee — CoffeeScript expression compilation bridge
3
+ var _coffeeScriptAvailable, _coffeeScriptUnavailableReason, _require, checkCoffeeScript, collapseWs, isSingleExpression, loadCoffeeScript, objectTarget, splitTopLevelStatements, stripCoffeeWrapper, stripToConst,
4
+ indexOf = [].indexOf;
4
5
 
5
6
  import {
6
7
  createRequire
@@ -178,9 +179,71 @@ export var compileStatement = function(source, location = null) {
178
179
  return source;
179
180
  };
180
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
+
181
231
  // ─── Convert var → const/let ───────────────────────────────
232
+
233
+ // Extract the bound variable name from one entry of an object
234
+ // destructuring pattern — shorthand `x` stays `x`, aliased `key: target`
235
+ // yields `target`.
236
+ objectTarget = function(pair) {
237
+ pair = pair.trim();
238
+ if (pair.includes(':')) {
239
+ return pair.split(':')[1].trim();
240
+ } else {
241
+ return pair;
242
+ }
243
+ };
244
+
182
245
  stripToConst = function(js) {
183
- var assign, assignments, body, idx, j, len, name, stmts, varIdx, varNames;
246
+ var allConsumed, body, constReplacements, consumed, idx, inner, j, len, m, n, name, names, s, stmts, varIdx, varNames;
184
247
  stmts = splitTopLevelStatements(js);
185
248
  if (stmts.length === 0) {
186
249
  return js.replace(/;\s*$/, '');
@@ -194,28 +257,96 @@ stripToConst = function(js) {
194
257
  varNames = stmts[varIdx].replace(/^\s*var\s+/, '').replace(/;\s*$/, '').split(',').map(function(s) {
195
258
  return s.trim();
196
259
  }).filter(Boolean);
197
- body = stmts.filter(function(s, i) {
198
- return i !== varIdx;
199
- });
200
- assignments = [];
201
- for (j = 0, len = varNames.length; j < len; j++) {
202
- name = varNames[j];
203
- idx = body.findIndex(function(s) {
204
- var t;
205
- t = s.trim();
206
- return t.startsWith(`${name} =`) || t.startsWith(`${name}=`);
260
+ body = (function() {
261
+ var j, len, ref, results;
262
+ ref = stmts.filter(function(s, i) {
263
+ return i !== varIdx;
264
+ });
265
+ results = [];
266
+ for (j = 0, len = ref.length; j < len; j++) {
267
+ s = ref[j];
268
+ results.push(collapseWs(s).replace(/;\s*$/, ''));
269
+ }
270
+ return results;
271
+ })();
272
+ constReplacements = {};
273
+ consumed = [];
274
+ for (idx = j = 0, len = body.length; j < len; idx = ++j) {
275
+ s = body[idx];
276
+ m = s.match(/^\[([^\]]*)\]\s*=/);
277
+ if (m != null) {
278
+ // Array destructuring: [a, b] = expr
279
+ names = (function() {
280
+ var k, len1, ref, results;
281
+ ref = m[1].split(',');
282
+ results = [];
283
+ for (k = 0, len1 = ref.length; k < len1; k++) {
284
+ n = ref[k];
285
+ if (n.trim()) {
286
+ results.push(n.trim());
287
+ }
288
+ }
289
+ return results;
290
+ })();
291
+ consumed = consumed.concat(names);
292
+ constReplacements[idx] = 'const ' + s;
293
+ continue;
294
+ }
295
+ m = s.match(/^\(\s*\{([^}]*)\}\s*=.*\)$/);
296
+ if (m != null) {
297
+ // Object destructuring: ({x, y} = expr) -> const {x, y} = expr
298
+ inner = s.replace(/^\s*\(\s*/, '').replace(/\s*\)\s*$/, '');
299
+ names = (function() {
300
+ var k, len1, ref, results;
301
+ ref = m[1].split(',');
302
+ results = [];
303
+ for (k = 0, len1 = ref.length; k < len1; k++) {
304
+ n = ref[k];
305
+ if (n.trim()) {
306
+ results.push(objectTarget(n));
307
+ }
308
+ }
309
+ return results;
310
+ })();
311
+ consumed = consumed.concat(names);
312
+ constReplacements[idx] = 'const ' + inner;
313
+ continue;
314
+ }
315
+ name = varNames.find(function(n) {
316
+ return s.startsWith(`${n} =`) || s.startsWith(`${n}=`);
207
317
  });
208
- if (idx >= 0) {
209
- assign = collapseWs(body[idx]).replace(/;\s*$/, '');
210
- assignments.push('const ' + assign);
318
+ if (name != null) {
319
+ consumed.push(name);
320
+ constReplacements[idx] = 'const ' + s;
211
321
  }
212
322
  }
213
- if (assignments.length > 0) {
214
- return assignments.join('; ');
323
+ allConsumed = varNames.every(function(n) {
324
+ return indexOf.call(consumed, n) >= 0;
325
+ });
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('; ');
215
338
  }
216
- return body.map(function(s) {
217
- return collapseWs(s);
218
- }).join(' ');
339
+ return ((function() {
340
+ var k, len1, results;
341
+ results = [];
342
+ for (k = 0, len1 = stmts.length; k < len1; k++) {
343
+ s = stmts[k];
344
+ // Fallback: keep the original var declaration so no variable goes
345
+ // undeclared (e.g. chained assignment `a = b = 1`).
346
+ results.push(collapseWs(s).replace(/;\s*$/, ''));
347
+ }
348
+ return results;
349
+ })()).join('; ');
219
350
  };
220
351
 
221
352
  // Split compiled JS into top-level statements, respecting string literals
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,8 @@ 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',
24
26
  DOCTYPE: 'DOCTYPE',
25
27
  TEXT: 'TEXT',
26
28
  PROLOGUE: 'PROLOGUE',
@@ -75,21 +77,23 @@ preMergeBracketLines = function(lines) {
75
77
  };
76
78
 
77
79
  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;
80
+ var body, bodyLine, content, currentIndent, fence, fenceIndent, fenceStartLine, fenceStartOffset, i, inPrologue, indent, indentStack, line, lineIndex, lineLength, lineStartOffset, lineTokens, lines, offset, rawLine, tokens;
79
81
  tokens = [];
80
82
  lines = preMergeBracketLines(source.split('\n'));
81
83
  indentStack = [0];
82
84
  offset = 0;
83
85
  lineIndex = 0;
84
86
  inPrologue = true;
85
- for (j = 0, len = lines.length; j < len; j++) {
86
- rawLine = lines[j];
87
+ i = 0;
88
+ while (i < lines.length) {
89
+ rawLine = lines[i];
87
90
  line = rawLine.replace(/\r$/, '');
88
91
  lineLength = rawLine.length + 1;
89
92
  // Skip empty lines
90
93
  if (line.trim() === '') {
91
94
  offset += lineLength;
92
95
  lineIndex++;
96
+ i++;
93
97
  continue;
94
98
  }
95
99
  indent = countIndent(line);
@@ -97,6 +101,73 @@ export var tokenize = function(source, filename = null) {
97
101
  if (content === '') {
98
102
  offset += lineLength;
99
103
  lineIndex++;
104
+ i++;
105
+ continue;
106
+ }
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).
114
+ fence = content.trim();
115
+ if (fence === '---' || fence === '===') {
116
+ inPrologue = false;
117
+ fenceIndent = indent;
118
+ fenceStartLine = lineIndex;
119
+ fenceStartOffset = offset + indent;
120
+ // Handle indentation change (same as a normal line)
121
+ currentIndent = indentStack[indentStack.length - 1];
122
+ if (fenceIndent > currentIndent) {
123
+ indentStack.push(fenceIndent);
124
+ tokens.push(indentToken(fenceIndent, fenceStartOffset, fenceIndent, filename, fenceStartLine));
125
+ } else if (fenceIndent < currentIndent) {
126
+ while (indentStack.length > 1 && fenceIndent < indentStack[indentStack.length - 1]) {
127
+ indentStack.pop();
128
+ tokens.push(dedentToken(fenceStartOffset, filename, fenceStartLine));
129
+ }
130
+ if (fenceIndent !== indentStack[indentStack.length - 1]) {
131
+ indentStack.push(fenceIndent);
132
+ tokens.push(indentToken(fenceIndent, fenceStartOffset, fenceIndent, filename, fenceStartLine));
133
+ }
134
+ }
135
+ // Consume opening fence
136
+ offset += lineLength;
137
+ lineIndex++;
138
+ i++;
139
+ // Collect body until closing fence
140
+ body = [];
141
+ while (i < lines.length) {
142
+ bodyLine = lines[i].replace(/\r$/, '');
143
+ if (bodyLine.trim() === fence) {
144
+ offset += lines[i].length + 1;
145
+ lineIndex++;
146
+ i++;
147
+ break;
148
+ }
149
+ body.push(bodyLine);
150
+ offset += lines[i].length + 1;
151
+ lineIndex++;
152
+ i++;
153
+ }
154
+ tokens.push({
155
+ type: (fence === '===' ? TokenType.COFFEE_YIELD : TokenType.COFFEE_BLOCK),
156
+ value: (dedentLines(body)).join('\n'),
157
+ location: {
158
+ start: {
159
+ line: fenceStartLine,
160
+ column: fenceIndent
161
+ },
162
+ end: {
163
+ line: lineIndex,
164
+ column: 0
165
+ },
166
+ offset: fenceStartOffset,
167
+ length: offset - fenceStartOffset,
168
+ file: filename
169
+ }
170
+ });
100
171
  continue;
101
172
  }
102
173
  // Prologue detection: non-indented JS before first HAML construct
@@ -120,6 +191,7 @@ export var tokenize = function(source, filename = null) {
120
191
  });
121
192
  offset += lineLength;
122
193
  lineIndex++;
194
+ i++;
123
195
  continue;
124
196
  }
125
197
  inPrologue = false;
@@ -144,6 +216,7 @@ export var tokenize = function(source, filename = null) {
144
216
  tokens.push(...lineTokens);
145
217
  offset += lineLength;
146
218
  lineIndex++;
219
+ i++;
147
220
  }
148
221
  // Emit remaining DEDENT tokens at EOF
149
222
  while (indentStack.length > 1) {
@@ -535,6 +608,52 @@ countIndent = function(line) {
535
608
  return count;
536
609
  };
537
610
 
611
+ // Strip up to `amount` visual columns of leading whitespace (space=1, tab=2).
612
+ stripIndent = function(line, amount) {
613
+ var ch, i, seen;
614
+ seen = 0;
615
+ i = 0;
616
+ while (i < line.length && seen < amount) {
617
+ ch = line[i];
618
+ if (ch === ' ') {
619
+ seen += 1;
620
+ } else if (ch === '\t') {
621
+ seen += 2;
622
+ } else {
623
+ break;
624
+ }
625
+ i++;
626
+ }
627
+ return line.slice(i);
628
+ };
629
+
630
+ // Normalize a fenced block's body to column 0 by removing the minimum
631
+ // indentation found across its non-empty lines.
632
+ dedentLines = function(lines) {
633
+ var amount, ind, j, k, l, len, len1, min, results;
634
+ min = null;
635
+ for (j = 0, len = lines.length; j < len; j++) {
636
+ l = lines[j];
637
+ if (l.trim() === '') {
638
+ continue;
639
+ }
640
+ ind = countIndent(l);
641
+ if (min === null || ind < min) {
642
+ min = ind;
643
+ }
644
+ }
645
+ amount = min === null ? 0 : min;
646
+ if (amount === 0) {
647
+ return lines;
648
+ }
649
+ results = [];
650
+ for (k = 0, len1 = lines.length; k < len1; k++) {
651
+ l = lines[k];
652
+ results.push(stripIndent(l, amount));
653
+ }
654
+ return results;
655
+ };
656
+
538
657
  indentToken = function(_indent, offset, level, filename = null, line = 0) {
539
658
  return {
540
659
  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, 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,8 @@ import {
16
16
  ControlFlow,
17
17
  Comment,
18
18
  Filter,
19
+ CoffeeBlock,
20
+ CoffeeYield,
19
21
  Doctype,
20
22
  Expression,
21
23
  Attribute,
@@ -79,7 +81,7 @@ ParserState = (function() {
79
81
  var t;
80
82
  while (this.current()) {
81
83
  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) {
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) {
83
85
  return;
84
86
  }
85
87
  this.advance();
@@ -168,6 +170,10 @@ parseNode = function(state) {
168
170
  return parseHtmlComment(state);
169
171
  case TokenType.FILTER:
170
172
  return parseFilter(state);
173
+ case TokenType.COFFEE_BLOCK:
174
+ return parseCoffeeBlock(state);
175
+ case TokenType.COFFEE_YIELD:
176
+ return parseCoffeeYield(state);
171
177
  case TokenType.DOCTYPE:
172
178
  return parseDoctype(state);
173
179
  case TokenType.TEXT:
@@ -461,6 +467,25 @@ parseFilter = function(state) {
461
467
  return new Filter(filterName, content, token.location);
462
468
  };
463
469
 
470
+ // ─── CoffeeBlock ───────────────────────────────────────────
471
+ parseCoffeeBlock = function(state) {
472
+ var token;
473
+ token = state.expect(TokenType.COFFEE_BLOCK);
474
+ if (!token) {
475
+ return new CoffeeBlock('');
476
+ }
477
+ return new CoffeeBlock(token.value, token.location);
478
+ };
479
+
480
+ parseCoffeeYield = function(state) {
481
+ var token;
482
+ token = state.expect(TokenType.COFFEE_YIELD);
483
+ if (!token) {
484
+ return new CoffeeYield('');
485
+ }
486
+ return new CoffeeYield(token.value, token.location);
487
+ };
488
+
464
489
  // ─── Doctype ───────────────────────────────────────────────
465
490
  parseDoctype = function(state) {
466
491
  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,