coffeehaml 0.6.0 → 0.7.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/dist/ast.d.ts +6 -0
- package/dist/ast.d.ts.map +1 -1
- package/dist/ast.js +2 -0
- package/dist/ast.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/parser.js +8 -2
- package/dist/parser.js.map +1 -1
- package/dist/prettier/coffeescript-formatter.d.ts +31 -0
- package/dist/prettier/coffeescript-formatter.d.ts.map +1 -0
- package/dist/prettier/coffeescript-formatter.js +409 -0
- package/dist/prettier/coffeescript-formatter.js.map +1 -0
- package/dist/prettier/index.d.ts +8 -0
- package/dist/prettier/index.d.ts.map +1 -0
- package/dist/prettier/index.js +69 -0
- package/dist/prettier/index.js.map +1 -0
- package/dist/prettier/options.d.ts +10 -0
- package/dist/prettier/options.d.ts.map +1 -0
- package/dist/prettier/options.js +200 -0
- package/dist/prettier/options.js.map +1 -0
- package/dist/prettier/printer.d.ts +45 -0
- package/dist/prettier/printer.d.ts.map +1 -0
- package/dist/prettier/printer.js +652 -0
- package/dist/prettier/printer.js.map +1 -0
- package/docs/examples.md +201 -1
- package/docs/grammar.md +90 -1
- package/docs/index.html +260 -37
- package/package.json +10 -3
- package/syntaxes/CoffeeHaml.tmLanguage +815 -868
- package/syntaxes/coffeehaml.tmLanguage.json +352 -161
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CoffeeHaml Prettier Printer — AST to Prettier Doc IR.
|
|
3
|
+
*
|
|
4
|
+
* Uses Prettier's `print` callback for recursive descent via path.map.
|
|
5
|
+
* Each formatting decision gated by options for independent deactivation.
|
|
6
|
+
*
|
|
7
|
+
* Note: uses `any` for Path types because the abstract Node base class
|
|
8
|
+
* doesn't declare `children`, so Prettier's typed AstPath.map can't
|
|
9
|
+
* resolve property access chains on our AST nodes.
|
|
10
|
+
*/
|
|
11
|
+
import * as prettier from 'prettier';
|
|
12
|
+
import { Element, ImplicitDiv, Text, Output, ControlFlow, Filter, Expression, } from '../ast.js';
|
|
13
|
+
import { formatCoffeeScript, formatCoffeeScriptBlock } from './coffeescript-formatter.js';
|
|
14
|
+
const { group, indent, hardline, join } = prettier.doc.builders;
|
|
15
|
+
// ─── Utilities ─────────────────────────────────────────────
|
|
16
|
+
function opts(p) {
|
|
17
|
+
return p.__opts || defaultOpts;
|
|
18
|
+
}
|
|
19
|
+
const defaultOpts = {
|
|
20
|
+
tagCase: 'preserve', implicitDivExpansion: false, maxChainLength: 4,
|
|
21
|
+
inlineThreshold: -1, voidElementStyle: 'self-closing',
|
|
22
|
+
attributeStyle: 'preserve', attributeMultilineThreshold: 1,
|
|
23
|
+
attributeSort: 'none', quoteStyle: 'preserve',
|
|
24
|
+
coffeeScriptFormat: true, methodChainAlign: true,
|
|
25
|
+
blankLineHandling: 'preserve', trailingWhitespace: 'remove',
|
|
26
|
+
continuationStyle: 'indent', controlFlowInline: false,
|
|
27
|
+
statementMerging: 'preserve', commentFormat: false,
|
|
28
|
+
tabWidth: 2, useTabs: false, printWidth: 80,
|
|
29
|
+
originalText: undefined,
|
|
30
|
+
};
|
|
31
|
+
/** Internal wrapper: merged consecutive childless statement nodes (formatting only). */
|
|
32
|
+
class MergedStatements {
|
|
33
|
+
constructor(stmts) {
|
|
34
|
+
this.kind = 'MergedStatements';
|
|
35
|
+
this.statements = stmts;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const VOID_ELEMENTS = new Set([
|
|
39
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
40
|
+
'link', 'meta', 'param', 'source', 'track', 'wbr',
|
|
41
|
+
]);
|
|
42
|
+
function isVoidElement(tag) { return VOID_ELEMENTS.has(tag.toLowerCase()); }
|
|
43
|
+
/** True if children is a single inline-able node (Text or Output). */
|
|
44
|
+
function isSingleInlineChild(children) {
|
|
45
|
+
if (children.length !== 1)
|
|
46
|
+
return false;
|
|
47
|
+
const child = children[0];
|
|
48
|
+
return child instanceof Text || child instanceof Output;
|
|
49
|
+
}
|
|
50
|
+
/** Join children docs with blank-line-aware spacing. */
|
|
51
|
+
function joinChildrenWithBlanks(nodes, docs, o) {
|
|
52
|
+
if (docs.length === 0)
|
|
53
|
+
return '';
|
|
54
|
+
if (docs.length === 1)
|
|
55
|
+
return docs[0];
|
|
56
|
+
const result = [];
|
|
57
|
+
for (let i = 0; i < docs.length; i++) {
|
|
58
|
+
if (i > 0) {
|
|
59
|
+
const node = nodes[i];
|
|
60
|
+
const hasBlank = o.blankLineHandling === 'preserve' && node._blankBefore;
|
|
61
|
+
result.push(hasBlank ? [hardline, hardline] : hardline);
|
|
62
|
+
}
|
|
63
|
+
result.push(docs[i]);
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
/** Format a single inline child as a Doc. */
|
|
68
|
+
function formatInlineChild(child, o) {
|
|
69
|
+
if (child instanceof Text)
|
|
70
|
+
return child.value;
|
|
71
|
+
if (child instanceof Output) {
|
|
72
|
+
const prefix = child.outputKind === 'unescaped' ? '!= ' : '= ';
|
|
73
|
+
return prefix + (o.coffeeScriptFormat
|
|
74
|
+
? formatCS(child.expression.source, o)
|
|
75
|
+
: child.expression.source);
|
|
76
|
+
}
|
|
77
|
+
return '';
|
|
78
|
+
}
|
|
79
|
+
function normalizeTag(tag, o) {
|
|
80
|
+
if (o.tagCase === 'lowercase') {
|
|
81
|
+
// Distinguish HTML tags from React components:
|
|
82
|
+
// - PascalCase (e.g., MyComponent) → component, preserve case
|
|
83
|
+
// - all-uppercase (e.g., DIV) → HTML tag, lowercase
|
|
84
|
+
// - all-lowercase (e.g., div) → HTML tag, lowercase
|
|
85
|
+
const isPascal = /^[A-Z][a-z]/.test(tag); // starts with uppercase followed by lowercase
|
|
86
|
+
if (!isPascal) {
|
|
87
|
+
return tag.toLowerCase();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return tag;
|
|
91
|
+
}
|
|
92
|
+
function buildTagPrefix(tag, classes, id, o) {
|
|
93
|
+
const parts = [];
|
|
94
|
+
if (tag)
|
|
95
|
+
parts.push('%' + normalizeTag(tag, o));
|
|
96
|
+
for (const c of classes)
|
|
97
|
+
parts.push('.' + c);
|
|
98
|
+
if (id)
|
|
99
|
+
parts.push('#' + id);
|
|
100
|
+
return parts.join('');
|
|
101
|
+
}
|
|
102
|
+
function formatAttr(attr) {
|
|
103
|
+
return attr.shorthand ? attr.name : `${attr.name}: ${attr.value.toString()}`;
|
|
104
|
+
}
|
|
105
|
+
function sortAttrs(attrs, o) {
|
|
106
|
+
if (o.attributeSort === 'none')
|
|
107
|
+
return attrs;
|
|
108
|
+
const staticAttrs = [];
|
|
109
|
+
const spreadAttrs = [];
|
|
110
|
+
for (const a of attrs) {
|
|
111
|
+
if ('spread' in a && a.spread)
|
|
112
|
+
spreadAttrs.push(a);
|
|
113
|
+
else
|
|
114
|
+
staticAttrs.push(a);
|
|
115
|
+
}
|
|
116
|
+
if (o.attributeSort === 'alphabetical') {
|
|
117
|
+
staticAttrs.sort((a, b) => a.name.localeCompare(b.name));
|
|
118
|
+
}
|
|
119
|
+
else if (o.attributeSort === 'idiomatic') {
|
|
120
|
+
staticAttrs.sort((a, b) => {
|
|
121
|
+
if (a.name === 'id')
|
|
122
|
+
return -1;
|
|
123
|
+
if (b.name === 'id')
|
|
124
|
+
return 1;
|
|
125
|
+
if (a.name === 'class')
|
|
126
|
+
return -1;
|
|
127
|
+
if (b.name === 'class')
|
|
128
|
+
return 1;
|
|
129
|
+
return a.name.localeCompare(b.name);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return [...staticAttrs, ...spreadAttrs];
|
|
133
|
+
}
|
|
134
|
+
// ─── Main dispatcher ───────────────────────────────────────
|
|
135
|
+
export function print(path, options, printFn) {
|
|
136
|
+
path.__opts = options;
|
|
137
|
+
if (options.statementMerging === 'merge') {
|
|
138
|
+
mergeAllStatements(path.getValue(), options);
|
|
139
|
+
}
|
|
140
|
+
if (options.blankLineHandling === 'preserve' && options.originalText) {
|
|
141
|
+
markBlankLines(path.getValue(), options.originalText);
|
|
142
|
+
}
|
|
143
|
+
return printNode(path, printFn);
|
|
144
|
+
}
|
|
145
|
+
/** Pre-pass: mark sibling nodes that had blank lines before them in source. */
|
|
146
|
+
function markBlankLines(node, source) {
|
|
147
|
+
if (!node || !node.children || !Array.isArray(node.children))
|
|
148
|
+
return;
|
|
149
|
+
const children = node.children;
|
|
150
|
+
for (let i = 1; i < children.length; i++) {
|
|
151
|
+
const prev = children[i - 1];
|
|
152
|
+
const curr = children[i];
|
|
153
|
+
const prevEnd = prev?.location?.offset + prev?.location?.length;
|
|
154
|
+
const currStart = curr?.location?.offset;
|
|
155
|
+
if (prevEnd != null && currStart != null && prevEnd < currStart) {
|
|
156
|
+
const between = source.slice(prevEnd, currStart);
|
|
157
|
+
// Two consecutive newlines (or more) = blank line
|
|
158
|
+
if (/\n\s*\n/.test(between)) {
|
|
159
|
+
curr._blankBefore = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Recurse
|
|
164
|
+
for (const child of children) {
|
|
165
|
+
if (!(child instanceof MergedStatements)) {
|
|
166
|
+
markBlankLines(child, source);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function printNode(path, printFn) {
|
|
171
|
+
const node = path.getValue();
|
|
172
|
+
if (!node || typeof node !== 'object')
|
|
173
|
+
return '';
|
|
174
|
+
switch (node.kind) {
|
|
175
|
+
case 'Document': return printDoc(path, printFn);
|
|
176
|
+
case 'Element': return printEl(path, printFn);
|
|
177
|
+
case 'ImplicitDiv': return printImplicit(path, printFn);
|
|
178
|
+
case 'Text': return node.value;
|
|
179
|
+
case 'Output': return printOut(path, printFn);
|
|
180
|
+
case 'ControlFlow': return printFlow(path, printFn);
|
|
181
|
+
case 'MergedStatements': return printMergedFlow(path, printFn);
|
|
182
|
+
case 'Comment': return printComm(path);
|
|
183
|
+
case 'Filter': return printFilt(path);
|
|
184
|
+
case 'Doctype': return '!!! ' + node.value;
|
|
185
|
+
default: return '';
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// ─── Document ──────────────────────────────────────────────
|
|
189
|
+
function printDoc(path, printFn) {
|
|
190
|
+
const doc = path.getValue();
|
|
191
|
+
const o = opts(path);
|
|
192
|
+
const docs = [];
|
|
193
|
+
if (doc.prologue && doc.prologue.length > 0) {
|
|
194
|
+
for (const line of doc.prologue) {
|
|
195
|
+
docs.push(line);
|
|
196
|
+
docs.push(hardline);
|
|
197
|
+
}
|
|
198
|
+
docs.push(hardline);
|
|
199
|
+
}
|
|
200
|
+
if (doc.children.length > 0) {
|
|
201
|
+
const childDocs = path.map(printFn, 'children');
|
|
202
|
+
docs.push(joinChildrenWithBlanks(doc.children, childDocs, o));
|
|
203
|
+
}
|
|
204
|
+
docs.push(hardline);
|
|
205
|
+
return docs;
|
|
206
|
+
}
|
|
207
|
+
// ─── Element ───────────────────────────────────────────────
|
|
208
|
+
function printEl(path, printFn) {
|
|
209
|
+
const node = path.getValue();
|
|
210
|
+
const o = opts(path);
|
|
211
|
+
const prefix = buildTagPrefix(node.tag instanceof Expression ? null : node.tag, node.classes, node.id, o);
|
|
212
|
+
const voidEl = !(node.tag instanceof Expression) && isVoidElement(node.tag);
|
|
213
|
+
const hasChildren = node.children.length > 0;
|
|
214
|
+
// Determine self-closing behavior
|
|
215
|
+
const shouldSelfClose = node.isSelfClosing ||
|
|
216
|
+
(voidEl && o.voidElementStyle === 'self-closing' && !hasChildren);
|
|
217
|
+
// explicit style: only self-close if author wrote %tag/
|
|
218
|
+
const showSlash = node.isSelfClosing ||
|
|
219
|
+
(voidEl && o.voidElementStyle === 'self-closing' && !hasChildren);
|
|
220
|
+
const tagDoc = node.tag instanceof Expression ? '%' + node.tag.source : prefix;
|
|
221
|
+
const attrsDoc = printAttrs(node.attributes, o, node);
|
|
222
|
+
if (shouldSelfClose && !hasChildren) {
|
|
223
|
+
return showSlash ? group([tagDoc, attrsDoc, '/']) : group([tagDoc, attrsDoc]);
|
|
224
|
+
}
|
|
225
|
+
if (!hasChildren) {
|
|
226
|
+
return group([tagDoc, attrsDoc]);
|
|
227
|
+
}
|
|
228
|
+
// Single inline child → keep inline
|
|
229
|
+
if (isSingleInlineChild(node.children)) {
|
|
230
|
+
const child = node.children[0];
|
|
231
|
+
const inlineDoc = formatInlineChild(child, o);
|
|
232
|
+
// Output (= expr) already has its own separator; Text needs a space
|
|
233
|
+
const sep = child instanceof Output ? '' : ' ';
|
|
234
|
+
return group([tagDoc, attrsDoc, sep, inlineDoc]);
|
|
235
|
+
}
|
|
236
|
+
const childDocs = path.map(printFn, 'children');
|
|
237
|
+
const childrenDoc = joinChildrenWithBlanks(node.children, childDocs, o);
|
|
238
|
+
return group([tagDoc, attrsDoc, indent([hardline, childrenDoc])]);
|
|
239
|
+
}
|
|
240
|
+
// ─── ImplicitDiv ───────────────────────────────────────────
|
|
241
|
+
function printImplicit(path, printFn) {
|
|
242
|
+
const node = path.getValue();
|
|
243
|
+
const o = opts(path);
|
|
244
|
+
const prefix = buildTagPrefix(null, node.classes, node.id, o);
|
|
245
|
+
if (node.classes.length > 1 && o.maxChainLength > 0 && node.classes.length >= o.maxChainLength) {
|
|
246
|
+
return printChained(path, printFn);
|
|
247
|
+
}
|
|
248
|
+
const attrsDoc = printAttrs(node.attributes, o, node);
|
|
249
|
+
if (node.children.length === 0)
|
|
250
|
+
return group([prefix, attrsDoc]);
|
|
251
|
+
// Single inline child → keep inline
|
|
252
|
+
if (isSingleInlineChild(node.children)) {
|
|
253
|
+
return group([prefix, attrsDoc, ' ', formatInlineChild(node.children[0], o)]);
|
|
254
|
+
}
|
|
255
|
+
const childDocs = path.map(printFn, 'children');
|
|
256
|
+
const childrenDoc = joinChildrenWithBlanks(node.children, childDocs, o);
|
|
257
|
+
return group([prefix, attrsDoc, indent([hardline, childrenDoc])]);
|
|
258
|
+
}
|
|
259
|
+
function printChained(path, printFn) {
|
|
260
|
+
const node = path.getValue();
|
|
261
|
+
const o = opts(path);
|
|
262
|
+
const docs = [];
|
|
263
|
+
for (let i = 0; i < node.classes.length; i++) {
|
|
264
|
+
if (i > 0)
|
|
265
|
+
docs.push(hardline);
|
|
266
|
+
const cls = node.classes[i];
|
|
267
|
+
const isLast = i === node.classes.length - 1;
|
|
268
|
+
const idPart = (isLast && node.id) ? '#' + node.id : '';
|
|
269
|
+
docs.push('.' + cls + idPart);
|
|
270
|
+
if (isLast) {
|
|
271
|
+
docs.push(printAttrs(node.attributes, o, node));
|
|
272
|
+
if (node.children.length > 0) {
|
|
273
|
+
const childDocs = path.map(printFn, 'children');
|
|
274
|
+
docs.push(indent([hardline, joinChildrenWithBlanks(node.children, childDocs, o)]));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return group(docs);
|
|
279
|
+
}
|
|
280
|
+
// ─── Output ────────────────────────────────────────────────
|
|
281
|
+
function printOut(path, printFn) {
|
|
282
|
+
const node = path.getValue();
|
|
283
|
+
const o = opts(path);
|
|
284
|
+
const prefix = node.outputKind === 'unescaped' ? '!= ' : '= ';
|
|
285
|
+
const exprDoc = o.coffeeScriptFormat
|
|
286
|
+
? formatCS(node.expression.source, o)
|
|
287
|
+
: node.expression.source;
|
|
288
|
+
if (node.children.length === 0)
|
|
289
|
+
return group([prefix, exprDoc]);
|
|
290
|
+
const childDocs = path.map(printFn, 'children');
|
|
291
|
+
const childrenDoc = joinChildrenWithBlanks(node.children, childDocs, o);
|
|
292
|
+
return group([prefix, exprDoc, indent([hardline, childrenDoc])]);
|
|
293
|
+
}
|
|
294
|
+
// ─── ControlFlow ───────────────────────────────────────────
|
|
295
|
+
function printFlow(path, printFn) {
|
|
296
|
+
const node = path.getValue();
|
|
297
|
+
const o = opts(path);
|
|
298
|
+
const keyword = node.controlKind;
|
|
299
|
+
const exprDoc = o.coffeeScriptFormat
|
|
300
|
+
? formatCS(node.expression.source, o)
|
|
301
|
+
: node.expression.source;
|
|
302
|
+
const hasChildren = node.children.length > 0;
|
|
303
|
+
const hasNext = node.next !== null;
|
|
304
|
+
// Inline control flow: - if x then .ok or - if x then %span hello
|
|
305
|
+
if (o.controlFlowInline && hasChildren && !hasNext) {
|
|
306
|
+
const exprHasNewlines = node.expression.source.includes('\n');
|
|
307
|
+
// Only if/unless support CoffeeScript's "then" keyword
|
|
308
|
+
const useThen = node.isConditional;
|
|
309
|
+
if (!exprHasNewlines && node.children.length === 1) {
|
|
310
|
+
const onlyChild = node.children[0];
|
|
311
|
+
// Text child: - if x then "hello"
|
|
312
|
+
if (onlyChild instanceof Text) {
|
|
313
|
+
const thenPart = useThen ? ' then ' : ' ';
|
|
314
|
+
return group(['- ', keyword, ' ', exprDoc, thenPart, onlyChild.value]);
|
|
315
|
+
}
|
|
316
|
+
// Element child: - if x then %span hello
|
|
317
|
+
if (onlyChild instanceof Element || onlyChild instanceof ImplicitDiv) {
|
|
318
|
+
const thenPart = useThen ? ' then ' : ' ';
|
|
319
|
+
const childDoc = nodeToDoc(onlyChild, o, printFn);
|
|
320
|
+
return group(['- ', keyword, ' ', exprDoc, thenPart, childDoc]);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const header = keyword === 'statement' ? ['-'] : ['- ', keyword];
|
|
325
|
+
if (node.expression.source) {
|
|
326
|
+
header.push(' ');
|
|
327
|
+
header.push(exprDoc);
|
|
328
|
+
}
|
|
329
|
+
const docs = [group(header)];
|
|
330
|
+
if (hasChildren) {
|
|
331
|
+
const childDocs = path.map(printFn, 'children');
|
|
332
|
+
docs.push(indent([hardline, joinChildrenWithBlanks(node.children, childDocs, o)]));
|
|
333
|
+
}
|
|
334
|
+
if (hasNext) {
|
|
335
|
+
docs.push(printNextFlow(node.next, o, printFn));
|
|
336
|
+
}
|
|
337
|
+
return group(docs);
|
|
338
|
+
}
|
|
339
|
+
function printNextFlow(next, o, printFn) {
|
|
340
|
+
const keyword = next.controlKind;
|
|
341
|
+
const exprDoc = o.coffeeScriptFormat
|
|
342
|
+
? formatCS(next.expression.source, o)
|
|
343
|
+
: next.expression.source;
|
|
344
|
+
const header = keyword === 'statement'
|
|
345
|
+
? [hardline, '-']
|
|
346
|
+
: [hardline, '- ', keyword];
|
|
347
|
+
if (next.expression.source) {
|
|
348
|
+
header.push(' ');
|
|
349
|
+
header.push(exprDoc);
|
|
350
|
+
}
|
|
351
|
+
const docs = [group(header)];
|
|
352
|
+
if (next.children.length > 0) {
|
|
353
|
+
docs.push(indent([hardline, printChildrenDirect(next.children, o, printFn)]));
|
|
354
|
+
}
|
|
355
|
+
if (next.next) {
|
|
356
|
+
docs.push(printNextFlow(next.next, o, printFn));
|
|
357
|
+
}
|
|
358
|
+
return group(docs);
|
|
359
|
+
}
|
|
360
|
+
// ─── Merged Statements (pre-pass) ──────────────────────────
|
|
361
|
+
function mergeAllStatements(node, o) {
|
|
362
|
+
if (!node || !node.children || !Array.isArray(node.children))
|
|
363
|
+
return;
|
|
364
|
+
node.children = mergeConsecutiveStatements(node.children, o);
|
|
365
|
+
for (const child of node.children) {
|
|
366
|
+
if (!(child instanceof MergedStatements)) {
|
|
367
|
+
mergeAllStatements(child, o);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function mergeConsecutiveStatements(children, o) {
|
|
372
|
+
if (o.statementMerging !== 'merge')
|
|
373
|
+
return children;
|
|
374
|
+
const result = [];
|
|
375
|
+
let stmtGroup = [];
|
|
376
|
+
for (const child of children) {
|
|
377
|
+
if (child instanceof ControlFlow &&
|
|
378
|
+
child.controlKind === 'statement' &&
|
|
379
|
+
child.children.length === 0 &&
|
|
380
|
+
!child.next) {
|
|
381
|
+
stmtGroup.push(child);
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
flushGroup();
|
|
385
|
+
result.push(child);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
flushGroup();
|
|
389
|
+
return result;
|
|
390
|
+
function flushGroup() {
|
|
391
|
+
if (stmtGroup.length > 1) {
|
|
392
|
+
result.push(new MergedStatements(stmtGroup));
|
|
393
|
+
}
|
|
394
|
+
else if (stmtGroup.length === 1) {
|
|
395
|
+
result.push(stmtGroup[0]);
|
|
396
|
+
}
|
|
397
|
+
stmtGroup = [];
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
function printMergedFlow(path, _printFn) {
|
|
401
|
+
const merged = path.getValue();
|
|
402
|
+
const o = opts(path);
|
|
403
|
+
const docs = ['-'];
|
|
404
|
+
for (const stmt of merged.statements) {
|
|
405
|
+
const exprDoc = o.coffeeScriptFormat
|
|
406
|
+
? formatCS(stmt.expression.source, o)
|
|
407
|
+
: stmt.expression.source;
|
|
408
|
+
docs.push(indent([hardline, exprDoc]));
|
|
409
|
+
}
|
|
410
|
+
return group(docs);
|
|
411
|
+
}
|
|
412
|
+
function printChildrenDirect(children, o, printFn) {
|
|
413
|
+
const docs = [];
|
|
414
|
+
for (let i = 0; i < children.length; i++) {
|
|
415
|
+
if (i > 0) {
|
|
416
|
+
const node = children[i];
|
|
417
|
+
const hasBlank = o.blankLineHandling === 'preserve' && node._blankBefore;
|
|
418
|
+
docs.push(hasBlank ? [hardline, hardline] : hardline);
|
|
419
|
+
}
|
|
420
|
+
docs.push(nodeToDoc(children[i], o, printFn));
|
|
421
|
+
}
|
|
422
|
+
return docs;
|
|
423
|
+
}
|
|
424
|
+
function nodeToDoc(node, o, printFn) {
|
|
425
|
+
if (node instanceof Text)
|
|
426
|
+
return node.value;
|
|
427
|
+
if (node instanceof Element) {
|
|
428
|
+
const prefix = buildTagPrefix(node.tag instanceof Expression ? null : node.tag, node.classes, node.id, o);
|
|
429
|
+
const attrsDoc = printAttrs(node.attributes, o, node);
|
|
430
|
+
if (isSingleInlineChild(node.children)) {
|
|
431
|
+
return group([prefix, attrsDoc, ' ', formatInlineChild(node.children[0], o)]);
|
|
432
|
+
}
|
|
433
|
+
const childrenDoc = node.children.length > 0
|
|
434
|
+
? indent([hardline, printChildrenDirect(node.children, o, printFn)])
|
|
435
|
+
: '';
|
|
436
|
+
return group([prefix, attrsDoc, childrenDoc]);
|
|
437
|
+
}
|
|
438
|
+
if (node instanceof ImplicitDiv) {
|
|
439
|
+
const prefix = buildTagPrefix(null, node.classes, node.id, o);
|
|
440
|
+
const attrsDoc = printAttrs(node.attributes, o, node);
|
|
441
|
+
if (isSingleInlineChild(node.children)) {
|
|
442
|
+
return group([prefix, attrsDoc, ' ', formatInlineChild(node.children[0], o)]);
|
|
443
|
+
}
|
|
444
|
+
const childrenDoc = node.children.length > 0
|
|
445
|
+
? indent([hardline, printChildrenDirect(node.children, o, printFn)])
|
|
446
|
+
: '';
|
|
447
|
+
return group([prefix, attrsDoc, childrenDoc]);
|
|
448
|
+
}
|
|
449
|
+
if (node instanceof Output) {
|
|
450
|
+
const prefix = node.outputKind === 'unescaped' ? '!= ' : '= ';
|
|
451
|
+
const exprDoc = o.coffeeScriptFormat
|
|
452
|
+
? formatCS(node.expression.source)
|
|
453
|
+
: node.expression.source;
|
|
454
|
+
const childrenDoc = node.children.length > 0
|
|
455
|
+
? indent([hardline, printChildrenDirect(node.children, o, printFn)])
|
|
456
|
+
: '';
|
|
457
|
+
return group([prefix, exprDoc, childrenDoc]);
|
|
458
|
+
}
|
|
459
|
+
if (node instanceof ControlFlow) {
|
|
460
|
+
const keyword = node.controlKind;
|
|
461
|
+
const exprDoc = o.coffeeScriptFormat
|
|
462
|
+
? formatCS(node.expression.source)
|
|
463
|
+
: node.expression.source;
|
|
464
|
+
const header = ['- ', keyword];
|
|
465
|
+
if (node.expression.source) {
|
|
466
|
+
header.push(' ');
|
|
467
|
+
header.push(exprDoc);
|
|
468
|
+
}
|
|
469
|
+
const childrenDoc = node.children.length > 0
|
|
470
|
+
? indent([hardline, printChildrenDirect(node.children, o, printFn)])
|
|
471
|
+
: '';
|
|
472
|
+
return group([...header, childrenDoc]);
|
|
473
|
+
}
|
|
474
|
+
return '';
|
|
475
|
+
}
|
|
476
|
+
// ─── Comment / Filter ──────────────────────────────────────
|
|
477
|
+
function printComm(path) {
|
|
478
|
+
const node = path.getValue();
|
|
479
|
+
const o = opts(path);
|
|
480
|
+
let text = node.text;
|
|
481
|
+
// Reflow long comment text to printWidth
|
|
482
|
+
if (o.commentFormat && text.length > o.printWidth - 3) {
|
|
483
|
+
text = reflowComment(text, o.printWidth - 3, 2);
|
|
484
|
+
}
|
|
485
|
+
return node.commentKind === 'html' ? '/ ' + text : '-# ' + text;
|
|
486
|
+
}
|
|
487
|
+
/** Simple word-wrap for comment text. Preserves existing newlines. */
|
|
488
|
+
function reflowComment(text, maxWidth, _indent) {
|
|
489
|
+
const paragraphs = text.split('\n');
|
|
490
|
+
const result = [];
|
|
491
|
+
for (const para of paragraphs) {
|
|
492
|
+
if (para.length <= maxWidth) {
|
|
493
|
+
result.push(para);
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
const words = para.split(/\s+/);
|
|
497
|
+
const lines = [];
|
|
498
|
+
let current = '';
|
|
499
|
+
for (const word of words) {
|
|
500
|
+
const candidate = current ? current + ' ' + word : word;
|
|
501
|
+
if (candidate.length <= maxWidth) {
|
|
502
|
+
current = candidate;
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
if (current)
|
|
506
|
+
lines.push(current);
|
|
507
|
+
current = word;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (current)
|
|
511
|
+
lines.push(current);
|
|
512
|
+
result.push(lines.join('\n'));
|
|
513
|
+
}
|
|
514
|
+
return result.join('\n');
|
|
515
|
+
}
|
|
516
|
+
function printFilt(path) {
|
|
517
|
+
const node = path.getValue();
|
|
518
|
+
const lines = node.content.split('\n');
|
|
519
|
+
const bodyDocs = [];
|
|
520
|
+
for (let i = 0; i < lines.length; i++) {
|
|
521
|
+
const l = lines[i];
|
|
522
|
+
// Skip leading empty line (from content starting with \n)
|
|
523
|
+
if (i === 0 && l === '')
|
|
524
|
+
continue;
|
|
525
|
+
bodyDocs.push(hardline);
|
|
526
|
+
bodyDocs.push(' ' + l);
|
|
527
|
+
}
|
|
528
|
+
if (bodyDocs.length > 0)
|
|
529
|
+
return [':' + node.filterName, ...bodyDocs];
|
|
530
|
+
return ':' + node.filterName;
|
|
531
|
+
}
|
|
532
|
+
// ─── Attributes ────────────────────────────────────────────
|
|
533
|
+
function printAttrs(attrs, o, element) {
|
|
534
|
+
if (attrs.length === 0)
|
|
535
|
+
return '';
|
|
536
|
+
const sorted = sortAttrs([...attrs], o);
|
|
537
|
+
const style = detectStyle(o, element);
|
|
538
|
+
if (style === 'bare')
|
|
539
|
+
return printBare(sorted, o);
|
|
540
|
+
const open = style === 'braces' ? '{' : '(';
|
|
541
|
+
const close = style === 'braces' ? '}' : ')';
|
|
542
|
+
if (sorted.length <= 1) {
|
|
543
|
+
const parts = [open];
|
|
544
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
545
|
+
if (i > 0)
|
|
546
|
+
parts.push(', ');
|
|
547
|
+
parts.push(anyAttr(sorted[i]));
|
|
548
|
+
}
|
|
549
|
+
parts.push(close);
|
|
550
|
+
return group(parts);
|
|
551
|
+
}
|
|
552
|
+
return group([
|
|
553
|
+
open,
|
|
554
|
+
indent([hardline, join([',', hardline], sorted.map(a => anyAttr(a)))]),
|
|
555
|
+
hardline,
|
|
556
|
+
close,
|
|
557
|
+
]);
|
|
558
|
+
}
|
|
559
|
+
function anyAttr(attr) {
|
|
560
|
+
if ('spread' in attr && attr.spread)
|
|
561
|
+
return '...' + attr.expression.source;
|
|
562
|
+
return formatAttr(attr);
|
|
563
|
+
}
|
|
564
|
+
function detectStyle(o, element) {
|
|
565
|
+
// When preserving, respect the original syntax from the AST
|
|
566
|
+
if (o.attributeStyle === 'preserve') {
|
|
567
|
+
if (element?.attrStyle)
|
|
568
|
+
return element.attrStyle;
|
|
569
|
+
// Fallback: if element has attributes but no recorded style, assume braces
|
|
570
|
+
if (element?.attributes && element.attributes.length > 0)
|
|
571
|
+
return 'braces';
|
|
572
|
+
return 'braces'; // default for elements with attributes
|
|
573
|
+
}
|
|
574
|
+
if (o.attributeStyle === 'parens')
|
|
575
|
+
return 'parens';
|
|
576
|
+
if (o.attributeStyle === 'bare')
|
|
577
|
+
return 'bare';
|
|
578
|
+
return 'braces';
|
|
579
|
+
}
|
|
580
|
+
function printBare(attrs, o) {
|
|
581
|
+
const parts = [];
|
|
582
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
583
|
+
if (i > 0)
|
|
584
|
+
parts.push(' ');
|
|
585
|
+
const attr = attrs[i];
|
|
586
|
+
if ('spread' in attr && attr.spread) {
|
|
587
|
+
parts.push('...' + attr.expression.source);
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
const q = o.quoteStyle === 'single' ? "'" : '"';
|
|
591
|
+
parts.push(attr.name + '=' + q + attr.value.toString() + q);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return group(parts);
|
|
595
|
+
}
|
|
596
|
+
// ─── CoffeeScript delegation (Phase 4) ────────────────────
|
|
597
|
+
/**
|
|
598
|
+
* Format a CoffeeScript expression string.
|
|
599
|
+
* Uses CoffeeScript's token-based formatter.
|
|
600
|
+
* Returns a Doc (string) that Prettier can embed.
|
|
601
|
+
*/
|
|
602
|
+
function formatCS(source, o) {
|
|
603
|
+
const pw = o?.printWidth ?? 80;
|
|
604
|
+
const mc = o?.methodChainAlign ?? true;
|
|
605
|
+
const enabled = o?.coffeeScriptFormat ?? true;
|
|
606
|
+
return formatCoffeeScript(source, {
|
|
607
|
+
printWidth: pw,
|
|
608
|
+
methodChainAlign: mc,
|
|
609
|
+
enabled,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Prettier embed function — formats full CoffeeScript blocks
|
|
614
|
+
* (Statement bodies, CoffeeScript filters) via the async textToDoc
|
|
615
|
+
* mechanism. Inline expressions are handled synchronously via formatCS.
|
|
616
|
+
*/
|
|
617
|
+
export function embedCoffeeScript(path, _printFn) {
|
|
618
|
+
const node = path.getValue();
|
|
619
|
+
const o = path.__opts;
|
|
620
|
+
// Statement children — when a - block has indented code continuation
|
|
621
|
+
// (these are Text nodes whose parent is a ControlFlow with controlKind === 'statement')
|
|
622
|
+
// We detect them by checking if this is a Text node inside a ControlFlow 'statement'
|
|
623
|
+
if (node instanceof Text) {
|
|
624
|
+
const parent = path.getParent?.();
|
|
625
|
+
if (parent instanceof ControlFlow && parent.controlKind === 'statement') {
|
|
626
|
+
const formatted = formatCoffeeScriptBlock(node.value, {
|
|
627
|
+
printWidth: o?.printWidth ?? 80,
|
|
628
|
+
methodChainAlign: o?.methodChainAlign ?? true,
|
|
629
|
+
enabled: o?.coffeeScriptFormat ?? true,
|
|
630
|
+
});
|
|
631
|
+
return formatted;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
// CoffeeScript filter: format the body, preserve the :coffeescript label
|
|
635
|
+
if (node instanceof Filter && node.filterName.toLowerCase() === 'coffeescript') {
|
|
636
|
+
const formatted = formatCoffeeScriptBlock(node.content, {
|
|
637
|
+
printWidth: o?.printWidth ?? 80,
|
|
638
|
+
methodChainAlign: o?.methodChainAlign ?? true,
|
|
639
|
+
enabled: o?.coffeeScriptFormat ?? true,
|
|
640
|
+
});
|
|
641
|
+
// Return full filter with label and indented body
|
|
642
|
+
const lines = formatted.split('\n');
|
|
643
|
+
const bodyDocs = [':' + node.filterName];
|
|
644
|
+
for (const l of lines) {
|
|
645
|
+
bodyDocs.push(hardline);
|
|
646
|
+
bodyDocs.push(' ' + l);
|
|
647
|
+
}
|
|
648
|
+
return bodyDocs;
|
|
649
|
+
}
|
|
650
|
+
return undefined;
|
|
651
|
+
}
|
|
652
|
+
//# sourceMappingURL=printer.js.map
|