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 +5 -1
- package/dist/ast.js +43 -1
- package/dist/cli.js +0 -0
- package/dist/emitter.js +59 -3
- package/dist/expressions.js +152 -21
- 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 +3 -3
- package/dist/coffeescript-formatter.js +0 -309
- package/dist/options.js +0 -232
- package/dist/printer.js +0 -583
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,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
|
|
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 =
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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 (
|
|
209
|
-
|
|
210
|
-
|
|
318
|
+
if (name != null) {
|
|
319
|
+
consumed.push(name);
|
|
320
|
+
constReplacements[idx] = 'const ' + s;
|
|
211
321
|
}
|
|
212
322
|
}
|
|
213
|
-
|
|
214
|
-
return
|
|
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
|
|
217
|
-
|
|
218
|
-
|
|
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,
|
|
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,
|