coffeehaml 0.7.7 → 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 +5 -1
- package/dist/ast.js +43 -1
- package/dist/cli.js +0 -0
- package/dist/emitter.js +59 -3
- package/dist/expressions.js +69 -10
- package/dist/lexer.js +123 -4
- package/dist/parser.js +27 -2
- package/dist/prettier/options.js +2 -2
- package/dist/prettier/printer.js +86 -39
- package/docs/grammar.md +66 -0
- package/package.json +2 -2
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.
|
|
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();
|
package/dist/expressions.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
272
|
+
constReplacements = {};
|
|
224
273
|
consumed = [];
|
|
225
|
-
for (j = 0, len = body.length; j < len; j
|
|
226
|
-
s = body[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
278
|
-
return
|
|
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,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,
|
|
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
|
-
|
|
86
|
-
|
|
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;
|
package/dist/prettier/options.js
CHANGED
|
@@ -81,7 +81,7 @@ export var options = {
|
|
|
81
81
|
attributeMultilineThreshold: {
|
|
82
82
|
category: CAT.ATTRIBUTES,
|
|
83
83
|
type: 'int',
|
|
84
|
-
description: '
|
|
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:
|
|
221
|
+
attributeMultilineThreshold: 0,
|
|
222
222
|
attributeSort: 'none',
|
|
223
223
|
quoteStyle: 'preserve',
|
|
224
224
|
coffeeScriptFormat: true,
|
package/dist/prettier/printer.js
CHANGED
|
@@ -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, 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,8 @@ import {
|
|
|
12
12
|
ControlFlow,
|
|
13
13
|
Comment,
|
|
14
14
|
Filter,
|
|
15
|
+
CoffeeBlock,
|
|
16
|
+
CoffeeYield,
|
|
15
17
|
Doctype,
|
|
16
18
|
Node,
|
|
17
19
|
Expression,
|
|
@@ -32,7 +34,7 @@ _require = createRequire(import.meta.url);
|
|
|
32
34
|
|
|
33
35
|
_prettier = _require('prettier');
|
|
34
36
|
|
|
35
|
-
({group, indent, hardline, join} = _prettier.doc.builders);
|
|
37
|
+
({group, indent, hardline, line, join} = _prettier.doc.builders);
|
|
36
38
|
|
|
37
39
|
// ─── Format Options ────────────────────────────────────────
|
|
38
40
|
// CoffeeHamlFormatOptions shape (runtime):
|
|
@@ -49,7 +51,7 @@ defaultOpts = {
|
|
|
49
51
|
inlineThreshold: -1,
|
|
50
52
|
voidElementStyle: 'self-closing',
|
|
51
53
|
attributeStyle: 'preserve',
|
|
52
|
-
attributeMultilineThreshold:
|
|
54
|
+
attributeMultilineThreshold: 0,
|
|
53
55
|
attributeSort: 'none',
|
|
54
56
|
quoteStyle: 'preserve',
|
|
55
57
|
coffeeScriptFormat: true,
|
|
@@ -225,6 +227,12 @@ printNode = function(path, o, printFn) {
|
|
|
225
227
|
if (node instanceof Filter) {
|
|
226
228
|
return printFilter(path, o, printFn);
|
|
227
229
|
}
|
|
230
|
+
if (node instanceof CoffeeBlock) {
|
|
231
|
+
return printCoffeeBlock(path, o, printFn);
|
|
232
|
+
}
|
|
233
|
+
if (node instanceof CoffeeYield) {
|
|
234
|
+
return printCoffeeYield(path, o, printFn);
|
|
235
|
+
}
|
|
228
236
|
if (node instanceof Doctype) {
|
|
229
237
|
return printDoctype(path, o);
|
|
230
238
|
}
|
|
@@ -259,7 +267,7 @@ printDoc = function(path, o, printFn) {
|
|
|
259
267
|
|
|
260
268
|
// ─── Element ───────────────────────────────────────────────
|
|
261
269
|
printElement = function(path, o, printFn) {
|
|
262
|
-
var attrStyle, attrs, childDoc, childDocs, children, classes, header, id, isVoid, joined, node, ref, ref1, ref2, ref3, selfClose, tag;
|
|
270
|
+
var attrStyle, attrs, childDoc, childDocs, children, classes, header, id, isVoid, joined, node, ref, ref1, ref2, ref3, selfClose, sep, tag;
|
|
263
271
|
node = path.node;
|
|
264
272
|
tag = formatTag(node.tag, o);
|
|
265
273
|
// Classes and ID
|
|
@@ -282,16 +290,18 @@ printElement = function(path, o, printFn) {
|
|
|
282
290
|
} else if (node.isSelfClosing) {
|
|
283
291
|
selfClose = '/';
|
|
284
292
|
}
|
|
285
|
-
header = `%${tag}${classes}${id}
|
|
293
|
+
header = [`%${tag}${classes}${id}`, attrs, selfClose];
|
|
286
294
|
// Children
|
|
287
295
|
children = (ref = node.children) != null ? ref : [];
|
|
288
296
|
if (children.length === 0) {
|
|
289
297
|
return header;
|
|
290
298
|
}
|
|
291
299
|
// Inline child
|
|
292
|
-
if (isSingleInlineChild(children)
|
|
300
|
+
if (isSingleInlineChild(children)) {
|
|
293
301
|
childDoc = (ref1 = (ref2 = path.map(printFn, 'children')) != null ? ref2[0] : void 0) != null ? ref1 : '';
|
|
294
|
-
|
|
302
|
+
// Output (= expr) carries its own separator; Text needs a space
|
|
303
|
+
sep = children[0] instanceof Output ? '' : ' ';
|
|
304
|
+
return [header, sep, childDoc];
|
|
295
305
|
}
|
|
296
306
|
childDocs = (ref3 = path.map(printFn, 'children')) != null ? ref3 : [];
|
|
297
307
|
joined = joinChildrenWithBlanks(children, childDocs, o);
|
|
@@ -300,7 +310,7 @@ printElement = function(path, o, printFn) {
|
|
|
300
310
|
|
|
301
311
|
// ─── ImplicitDiv ───────────────────────────────────────────
|
|
302
312
|
printImplicitDiv = function(path, o, printFn) {
|
|
303
|
-
var attrStyle, attrs, childDoc, childDocs, children, classes, header, id, joined, node, ref, ref1, ref2, ref3;
|
|
313
|
+
var attrStyle, attrs, childDoc, childDocs, children, classes, header, id, joined, node, ref, ref1, ref2, ref3, sep;
|
|
304
314
|
node = path.node;
|
|
305
315
|
classes = node.classes.map(function(c) {
|
|
306
316
|
return `.${c}`;
|
|
@@ -308,14 +318,15 @@ printImplicitDiv = function(path, o, printFn) {
|
|
|
308
318
|
id = node.id ? `#${node.id}` : '';
|
|
309
319
|
attrStyle = detectStyle(node, o);
|
|
310
320
|
attrs = formatAttributes(node, attrStyle, o);
|
|
311
|
-
header = `${classes}${id}
|
|
321
|
+
header = [`${classes}${id}`, attrs];
|
|
312
322
|
children = (ref = node.children) != null ? ref : [];
|
|
313
323
|
if (children.length === 0) {
|
|
314
324
|
return header;
|
|
315
325
|
}
|
|
316
326
|
if (isSingleInlineChild(children)) {
|
|
317
327
|
childDoc = (ref1 = (ref2 = path.map(printFn, 'children')) != null ? ref2[0] : void 0) != null ? ref1 : '';
|
|
318
|
-
|
|
328
|
+
sep = children[0] instanceof Output ? '' : ' ';
|
|
329
|
+
return [header, sep, childDoc];
|
|
319
330
|
}
|
|
320
331
|
childDocs = (ref3 = path.map(printFn, 'children')) != null ? ref3 : [];
|
|
321
332
|
joined = joinChildrenWithBlanks(children, childDocs, o);
|
|
@@ -473,6 +484,34 @@ printFilter = function(path, o, _printFn) {
|
|
|
473
484
|
}
|
|
474
485
|
};
|
|
475
486
|
|
|
487
|
+
// ─── CoffeeBlock ───────────────────────────────────────────
|
|
488
|
+
|
|
489
|
+
// Build a fenced block doc using `hardline` so Prettier can re-indent the
|
|
490
|
+
// dedented body to the correct nesting level (raw `\n` strings would not).
|
|
491
|
+
printFence = function(fence, content) {
|
|
492
|
+
var k, len, parts, ref;
|
|
493
|
+
if (content) {
|
|
494
|
+
parts = [fence];
|
|
495
|
+
ref = content.split('\n');
|
|
496
|
+
for (k = 0, len = ref.length; k < len; k++) {
|
|
497
|
+
line = ref[k];
|
|
498
|
+
parts.push(hardline, line);
|
|
499
|
+
}
|
|
500
|
+
parts.push(hardline, fence);
|
|
501
|
+
return parts;
|
|
502
|
+
} else {
|
|
503
|
+
return [fence, hardline, fence];
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
printCoffeeBlock = function(path, o, _printFn) {
|
|
508
|
+
return printFence('---', path.node.content);
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
printCoffeeYield = function(path, o, _printFn) {
|
|
512
|
+
return printFence('===', path.node.content);
|
|
513
|
+
};
|
|
514
|
+
|
|
476
515
|
// ─── Doctype ───────────────────────────────────────────────
|
|
477
516
|
printDoctype = function(path, o) {
|
|
478
517
|
var node;
|
|
@@ -501,12 +540,14 @@ detectStyle = function(node, o) {
|
|
|
501
540
|
};
|
|
502
541
|
|
|
503
542
|
formatAttributes = function(node, style, o) {
|
|
504
|
-
var attr, attrs, bareParts, expr, k, len, name, parts, ref, ref1, ref2, val;
|
|
543
|
+
var attr, attrs, bareParts, close, expr, forceBreak, k, len, name, open, parts, ref, ref1, ref2, sep, val;
|
|
505
544
|
attrs = (ref = node.attributes) != null ? ref : [];
|
|
506
545
|
if (!(attrs.length > 0)) {
|
|
507
546
|
return '';
|
|
508
547
|
}
|
|
509
|
-
// Bare style: HTML-style key="val" key2={expr} — space-separated
|
|
548
|
+
// Bare style: HTML-style key="val" key2={expr} — space-separated.
|
|
549
|
+
// Never folded: an indented continuation line would parse as a child
|
|
550
|
+
// element, silently changing the tree. Bare attributes stay inline.
|
|
510
551
|
if (style === 'bare') {
|
|
511
552
|
bareParts = (function() {
|
|
512
553
|
var k, len, ref1, ref2, results;
|
|
@@ -528,38 +569,44 @@ formatAttributes = function(node, style, o) {
|
|
|
528
569
|
return results;
|
|
529
570
|
})();
|
|
530
571
|
return ` ${bareParts.join(' ')}`;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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 : '';
|
|
572
|
+
}
|
|
573
|
+
parts = [];
|
|
574
|
+
for (k = 0, len = attrs.length; k < len; k++) {
|
|
575
|
+
attr = attrs[k];
|
|
576
|
+
if (attr.spread) {
|
|
577
|
+
expr = (ref1 = attr.expression.source) != null ? ref1 : '';
|
|
545
578
|
if (o.coffeeScriptFormat) {
|
|
546
|
-
|
|
547
|
-
}
|
|
548
|
-
if (attr.shorthand) {
|
|
549
|
-
parts.push(name);
|
|
550
|
-
} else {
|
|
551
|
-
parts.push(`${name}: ${val}`);
|
|
579
|
+
expr = formatCoffeeScript(expr, o);
|
|
552
580
|
}
|
|
581
|
+
parts.push(style === 'parens' ? `...${expr}` : `{${expr}...}`);
|
|
582
|
+
continue;
|
|
553
583
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
584
|
+
name = attr.name;
|
|
585
|
+
val = (ref2 = attr.value.source) != null ? ref2 : '';
|
|
586
|
+
if (o.coffeeScriptFormat) {
|
|
587
|
+
val = formatCoffeeScript(val, o);
|
|
588
|
+
}
|
|
589
|
+
if (attr.shorthand) {
|
|
590
|
+
parts.push(name);
|
|
591
|
+
} else {
|
|
592
|
+
parts.push(`${name}: ${val}`);
|
|
561
593
|
}
|
|
562
594
|
}
|
|
595
|
+
// Count-based forcing: a positive threshold breaks even when the
|
|
596
|
+
// attributes would otherwise fit within printWidth.
|
|
597
|
+
forceBreak = o.attributeMultilineThreshold > 0 && attrs.length >= o.attributeMultilineThreshold;
|
|
598
|
+
sep = forceBreak ? hardline : line;
|
|
599
|
+
if (style === 'braces') {
|
|
600
|
+
open = '{';
|
|
601
|
+
close = '}';
|
|
602
|
+
} else if (style === 'parens') {
|
|
603
|
+
open = '(';
|
|
604
|
+
close = ')';
|
|
605
|
+
} else {
|
|
606
|
+
// Unknown style — inline fallback, no folding.
|
|
607
|
+
return parts.join(', ');
|
|
608
|
+
}
|
|
609
|
+
return group([open, indent([sep, join([',', sep], parts)]), sep, close]);
|
|
563
610
|
};
|
|
564
611
|
|
|
565
612
|
// ─── CoffeeScript Embed (for Prettier embed) ───────────────
|
package/docs/grammar.md
CHANGED
|
@@ -292,6 +292,72 @@ 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
|
+
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.
|
|
311
|
+
|
|
312
|
+
```haml
|
|
313
|
+
---
|
|
314
|
+
import { blah } from 'ok'
|
|
315
|
+
answer = 42
|
|
316
|
+
---
|
|
317
|
+
%p= answer
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Semantics:
|
|
321
|
+
|
|
322
|
+
- **Escapes indentation**: body lines are dedented to column 0, so a block
|
|
323
|
+
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).
|
|
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 being hoisted to module scope 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
|
+
|
|
295
361
|
## Raw Text / Passthrough
|
|
296
362
|
|
|
297
363
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coffeehaml",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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
|
},
|