coffeehaml 0.5.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.
Files changed (44) hide show
  1. package/README.md +13 -4
  2. package/dist/ast.d.ts +6 -0
  3. package/dist/ast.d.ts.map +1 -1
  4. package/dist/ast.js +2 -0
  5. package/dist/ast.js.map +1 -1
  6. package/dist/cli.js +178 -2
  7. package/dist/cli.js.map +1 -1
  8. package/dist/compiler.js +8 -10
  9. package/dist/compiler.js.map +1 -1
  10. package/dist/emitter.js +81 -5
  11. package/dist/emitter.js.map +1 -1
  12. package/dist/index.d.ts +2 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +2 -0
  15. package/dist/index.js.map +1 -1
  16. package/dist/parser.d.ts +7 -1
  17. package/dist/parser.d.ts.map +1 -1
  18. package/dist/parser.js +46 -4
  19. package/dist/parser.js.map +1 -1
  20. package/dist/prettier/coffeescript-formatter.d.ts +31 -0
  21. package/dist/prettier/coffeescript-formatter.d.ts.map +1 -0
  22. package/dist/prettier/coffeescript-formatter.js +409 -0
  23. package/dist/prettier/coffeescript-formatter.js.map +1 -0
  24. package/dist/prettier/index.d.ts +8 -0
  25. package/dist/prettier/index.d.ts.map +1 -0
  26. package/dist/prettier/index.js +69 -0
  27. package/dist/prettier/index.js.map +1 -0
  28. package/dist/prettier/options.d.ts +10 -0
  29. package/dist/prettier/options.d.ts.map +1 -0
  30. package/dist/prettier/options.js +200 -0
  31. package/dist/prettier/options.js.map +1 -0
  32. package/dist/prettier/printer.d.ts +45 -0
  33. package/dist/prettier/printer.d.ts.map +1 -0
  34. package/dist/prettier/printer.js +652 -0
  35. package/dist/prettier/printer.js.map +1 -0
  36. package/dist/vite-plugin.d.ts.map +1 -1
  37. package/dist/vite-plugin.js +11 -1
  38. package/dist/vite-plugin.js.map +1 -1
  39. package/docs/examples.md +201 -1
  40. package/docs/grammar.md +90 -1
  41. package/docs/index.html +260 -37
  42. package/package.json +10 -3
  43. package/syntaxes/CoffeeHaml.tmLanguage +815 -868
  44. 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