coffeehaml 0.7.0 → 0.7.1

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 (73) hide show
  1. package/README.md +21 -5
  2. package/dist/ast.js +334 -125
  3. package/dist/cli.js +363 -291
  4. package/dist/compiler.js +126 -89
  5. package/dist/emitter.js +771 -736
  6. package/dist/errors.js +5 -2
  7. package/dist/expressions.js +211 -154
  8. package/dist/index.js +22 -6
  9. package/dist/lexer.js +536 -340
  10. package/dist/parser.js +533 -438
  11. package/dist/prettier/coffeescript-formatter.js +292 -392
  12. package/dist/prettier/index.js +96 -60
  13. package/dist/prettier/options.js +179 -147
  14. package/dist/prettier/printer.js +549 -642
  15. package/dist/src/ast.js +338 -0
  16. package/dist/src/cli.js +430 -0
  17. package/dist/src/compiler.js +132 -0
  18. package/dist/src/emitter.js +791 -0
  19. package/dist/src/errors.js +5 -0
  20. package/dist/src/expressions.js +223 -0
  21. package/dist/src/index.js +22 -0
  22. package/dist/src/lexer.js +504 -0
  23. package/dist/src/parser.js +575 -0
  24. package/dist/src/types.js +122 -0
  25. package/dist/src/vite-plugin.js +78 -0
  26. package/dist/types.js +122 -12
  27. package/dist/vite-plugin.js +78 -63
  28. package/package.json +4 -9
  29. package/dist/ast.d.ts +0 -123
  30. package/dist/ast.d.ts.map +0 -1
  31. package/dist/ast.js.map +0 -1
  32. package/dist/cli.d.ts +0 -3
  33. package/dist/cli.d.ts.map +0 -1
  34. package/dist/cli.js.map +0 -1
  35. package/dist/compiler.d.ts +0 -10
  36. package/dist/compiler.d.ts.map +0 -1
  37. package/dist/compiler.js.map +0 -1
  38. package/dist/emitter.d.ts +0 -4
  39. package/dist/emitter.d.ts.map +0 -1
  40. package/dist/emitter.js.map +0 -1
  41. package/dist/errors.d.ts +0 -3
  42. package/dist/errors.d.ts.map +0 -1
  43. package/dist/errors.js.map +0 -1
  44. package/dist/expressions.d.ts +0 -22
  45. package/dist/expressions.d.ts.map +0 -1
  46. package/dist/expressions.js.map +0 -1
  47. package/dist/index.d.ts +0 -7
  48. package/dist/index.d.ts.map +0 -1
  49. package/dist/index.js.map +0 -1
  50. package/dist/lexer.d.ts +0 -30
  51. package/dist/lexer.d.ts.map +0 -1
  52. package/dist/lexer.js.map +0 -1
  53. package/dist/parser.d.ts +0 -10
  54. package/dist/parser.d.ts.map +0 -1
  55. package/dist/parser.js.map +0 -1
  56. package/dist/prettier/coffeescript-formatter.d.ts +0 -31
  57. package/dist/prettier/coffeescript-formatter.d.ts.map +0 -1
  58. package/dist/prettier/coffeescript-formatter.js.map +0 -1
  59. package/dist/prettier/index.d.ts +0 -8
  60. package/dist/prettier/index.d.ts.map +0 -1
  61. package/dist/prettier/index.js.map +0 -1
  62. package/dist/prettier/options.d.ts +0 -10
  63. package/dist/prettier/options.d.ts.map +0 -1
  64. package/dist/prettier/options.js.map +0 -1
  65. package/dist/prettier/printer.d.ts +0 -45
  66. package/dist/prettier/printer.d.ts.map +0 -1
  67. package/dist/prettier/printer.js.map +0 -1
  68. package/dist/types.d.ts +0 -76
  69. package/dist/types.d.ts.map +0 -1
  70. package/dist/types.js.map +0 -1
  71. package/dist/vite-plugin.d.ts +0 -7
  72. package/dist/vite-plugin.d.ts.map +0 -1
  73. package/dist/vite-plugin.js.map +0 -1
@@ -1,652 +1,559 @@
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,
1
+ // Generated by CoffeeScript 2.7.0
2
+ // printer.coffee — AST → Prettier Doc IR
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;
5
+
6
+ import {
7
+ Document,
8
+ Element,
9
+ ImplicitDiv,
10
+ Text,
11
+ Output,
12
+ ControlFlow,
13
+ Comment,
14
+ Filter,
15
+ Doctype,
16
+ Node,
17
+ Expression,
18
+ Attribute,
19
+ SpreadAttribute
20
+ } from '../ast.js';
21
+
22
+ import {
23
+ formatCoffeeScript,
24
+ formatCoffeeScriptBlock
25
+ } from './coffeescript-formatter.js';
26
+
27
+ import {
28
+ createRequire
29
+ } from 'module';
30
+
31
+ _require = createRequire(import.meta.url);
32
+
33
+ _prettier = _require('prettier');
34
+
35
+ ({group, indent, hardline, join} = _prettier.doc.builders);
36
+
37
+ // ─── Format Options ────────────────────────────────────────
38
+ // CoffeeHamlFormatOptions shape (runtime):
39
+ // tagCase, implicitDivExpansion, maxChainLength, inlineThreshold
40
+ // voidElementStyle, attributeStyle, attributeMultilineThreshold
41
+ // attributeSort, quoteStyle, coffeeScriptFormat, methodChainAlign
42
+ // blankLineHandling, trailingWhitespace, continuationStyle
43
+ // controlFlowInline, statementMerging, commentFormat
44
+ // tabWidth, useTabs, printWidth, originalText?
45
+ defaultOpts = {
46
+ tagCase: 'preserve',
47
+ implicitDivExpansion: false,
48
+ maxChainLength: 4,
49
+ inlineThreshold: -1,
50
+ voidElementStyle: 'self-closing',
51
+ attributeStyle: 'preserve',
52
+ attributeMultilineThreshold: 1,
53
+ attributeSort: 'none',
54
+ quoteStyle: 'preserve',
55
+ coffeeScriptFormat: true,
56
+ methodChainAlign: true,
57
+ blankLineHandling: 'preserve',
58
+ trailingWhitespace: 'remove',
59
+ continuationStyle: 'indent',
60
+ controlFlowInline: false,
61
+ statementMerging: 'preserve',
62
+ commentFormat: false,
63
+ tabWidth: 2,
64
+ useTabs: false,
65
+ printWidth: 80,
66
+ originalText: void 0
30
67
  };
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
- }
68
+
69
+ opts = function(p) {
70
+ return p.__opts || defaultOpts;
71
+ };
72
+
73
+ MergedStatements = (function() {
74
+ // ─── MergedStatements wrapper ──────────────────────────────
75
+ class MergedStatements {
76
+ constructor(statements) {
77
+ this.statements = statements;
78
+ }
79
+
80
+ };
81
+
82
+ MergedStatements.prototype.kind = 'MergedStatements';
83
+
84
+ MergedStatements.prototype.statements = [];
85
+
86
+ return MergedStatements;
87
+
88
+ }).call(this);
89
+
90
+ // ─── Constants ─────────────────────────────────────────────
91
+ VOID_ELEMENTS = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
92
+
93
+ isVoidElement = function(tag) {
94
+ return VOID_ELEMENTS.has(tag.toLowerCase());
95
+ };
96
+
97
+ isSingleInlineChild = function(children) {
98
+ return children.length === 1 && (children[0] instanceof Text || children[0] instanceof Output);
99
+ };
100
+
101
+ // ─── Blank-line-aware join ─────────────────────────────────
102
+ joinChildrenWithBlanks = function(nodes, docs, o) {
103
+ var hasBlank, i, k, ref, ref1, result;
104
+ if (docs.length === 0) {
77
105
  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);
106
+ }
107
+ if (docs.length === 1) {
108
+ return docs[0];
109
+ }
110
+ result = [];
111
+ for (i = k = 0, ref = docs.length; (0 <= ref ? k < ref : k > ref); i = 0 <= ref ? ++k : --k) {
112
+ if (i > 0) {
113
+ hasBlank = ((ref1 = nodes[i]) != null ? ref1._blankBefore : void 0) || false;
114
+ result.push(hasBlank ? [hardline, hardline] : hardline);
115
+ }
116
+ result.push(docs[i]);
117
+ }
118
+ return result;
119
+ };
120
+
121
+ analyzeBlankLines = function(children, originalText, locations) {
122
+ var between, currStartOffset, i, k, node, prev, prevEndOffset, ref, results;
123
+ if (!(originalText && children.length > 1)) {
124
+ return;
125
+ }
126
+ results = [];
127
+ for (i = k = 0, ref = children.length; (0 <= ref ? k < ref : k > ref); i = 0 <= ref ? ++k : --k) {
128
+ node = children[i];
129
+ if (!(node != null ? node.location : void 0)) {
130
+ continue;
131
+ }
132
+ prev = children[i - 1];
133
+ if (!(prev != null ? prev.location : void 0)) {
134
+ continue;
135
+ }
136
+ // Check for blank line in source between prev end and this node start
137
+ prevEndOffset = prev.location.offset + prev.location.length;
138
+ currStartOffset = node.location.offset;
139
+ between = originalText.slice(prevEndOffset, currStartOffset);
140
+ if (/\n\s*\n/.test(between)) {
141
+ results.push(node._blankBefore = true);
142
+ } else {
143
+ results.push(void 0);
144
+ }
145
+ }
146
+ return results;
147
+ };
148
+
149
+ // ─── Merge consecutive statements ──────────────────────────
150
+ mergeAllStatements = function(children) {
151
+ var child, i, j, next, result;
152
+ if (!((children != null ? children.length : void 0) > 1)) {
153
+ return children;
154
+ }
155
+ result = [];
156
+ i = 0;
157
+ while (i < children.length) {
158
+ child = children[i];
159
+ if (child instanceof ControlFlow && child.controlKind === 'statement' && child.children.length === 0) {
160
+ group = [child];
161
+ j = i + 1;
162
+ while (j < children.length) {
163
+ next = children[j];
164
+ if (next instanceof ControlFlow && next.controlKind === 'statement' && next.children.length === 0) {
165
+ group.push(next);
166
+ j++;
167
+ } else {
168
+ break;
167
169
  }
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
- }
170
+ }
171
+ if (group.length > 1) {
172
+ result.push(new MergedStatements(group));
173
+ i = j;
174
+ continue;
175
+ }
176
+ }
177
+ result.push(child);
178
+ i++;
179
+ }
180
+ return result;
181
+ };
182
+
183
+ // ─── Main entry ────────────────────────────────────────────
184
+ export var print = function(path, o, printFn) {
185
+ var node;
186
+ node = path.node;
187
+ if (!node) {
188
+ return '';
189
+ }
190
+ // Hang options on path for nested lookups
191
+ path.__opts = o;
192
+ return printNode(path, o, printFn);
193
+ };
194
+
195
+ printNode = function(path, o, printFn) {
196
+ var node;
197
+ node = path.node;
198
+ if (!node) {
199
+ return '';
200
+ }
201
+ if (node instanceof Document) {
202
+ return printDoc(path, o, printFn);
203
+ }
204
+ if (node instanceof Element) {
205
+ return printElement(path, o, printFn);
206
+ }
207
+ if (node instanceof ImplicitDiv) {
208
+ return printImplicitDiv(path, o, printFn);
209
+ }
210
+ if (node instanceof Text) {
211
+ return node.value;
212
+ }
213
+ if (node instanceof Output) {
214
+ return printOutput(path, o, printFn);
215
+ }
216
+ if (node instanceof ControlFlow) {
217
+ return printControlFlow(path, o, printFn);
218
+ }
219
+ if (node instanceof MergedStatements) {
220
+ return printMergedFlow(path, o, printFn);
221
+ }
222
+ if (node instanceof Comment) {
223
+ return printComment(path, o, printFn);
224
+ }
225
+ if (node instanceof Filter) {
226
+ return printFilter(path, o, printFn);
227
+ }
228
+ if (node instanceof Doctype) {
229
+ return printDoctype(path, o);
230
+ }
231
+ return '';
232
+ };
233
+
188
234
  // ─── 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
- }
235
+ printDoc = function(path, o, printFn) {
236
+ var childDocs, k, len, n, next, node, ref, ref1;
237
+ node = path.node;
238
+ // Pre-pass: merge statements if enabled
239
+ if (o.statementMerging === 'merge') {
240
+ node.children = mergeAllStatements(node.children);
241
+ }
242
+ // Pre-pass: analyze blank lines
243
+ if (o.blankLineHandling === 'preserve' && o.originalText) {
244
+ ref = node.children;
245
+ for (k = 0, len = ref.length; k < len; k++) {
246
+ n = ref[k];
247
+ next = node.children[node.children.indexOf(n) + 1];
248
+ if (next) {
249
+ analyzeBlankLines([n, next], o.originalText);
250
+ }
251
+ }
252
+ }
253
+ // Print children
254
+ childDocs = (ref1 = path.map(printFn, 'children')) != null ? ref1 : [];
255
+ return join(hardline, childDocs);
256
+ };
257
+
207
258
  // ─── 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
- }
259
+ printElement = function(path, o, printFn) {
260
+ var attrStyle, attrs, childDoc, childDocs, children, classes, header, id, isVoid, joined, node, ref, ref1, ref2, ref3, selfClose, tag;
261
+ node = path.node;
262
+ tag = formatTag(node.tag, o);
263
+ // Classes and ID
264
+ classes = node.classes.map(function(c) {
265
+ return `.${c}`;
266
+ }).join('');
267
+ id = node.id ? `#${node.id}` : '';
268
+ // Attributes
269
+ attrStyle = detectStyle(node, o);
270
+ attrs = formatAttributes(node, attrStyle, o);
271
+ // Self-close marker
272
+ isVoid = typeof node.tag === 'string' && isVoidElement(node.tag);
273
+ selfClose = '';
274
+ if (isVoid) {
275
+ if (o.voidElementStyle === 'self-closing') {
276
+ selfClose = '/';
277
+ } else if (node.isSelfClosing) {
278
+ selfClose = '/';
279
+ }
280
+ } else if (node.isSelfClosing) {
281
+ selfClose = '/';
282
+ }
283
+ header = `%${tag}${classes}${id}${attrs}${selfClose}`;
284
+ // Children
285
+ children = (ref = node.children) != null ? ref : [];
286
+ if (children.length === 0) {
287
+ return header;
288
+ }
289
+ // Inline child
290
+ if (isSingleInlineChild(children) && o.controlFlowInline !== false) {
291
+ childDoc = (ref1 = (ref2 = path.map(printFn, 'children')) != null ? ref2[0] : void 0) != null ? ref1 : '';
292
+ return `${header} ${childDoc}`;
293
+ }
294
+ childDocs = (ref3 = path.map(printFn, 'children')) != null ? ref3 : [];
295
+ joined = joinChildrenWithBlanks(children, childDocs, o);
296
+ return join(hardline, [header, indent(joined)]);
297
+ };
298
+
240
299
  // ─── 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
- }
300
+ printImplicitDiv = function(path, o, printFn) {
301
+ var attrStyle, attrs, childDoc, childDocs, children, classes, header, id, joined, node, ref, ref1, ref2, ref3;
302
+ node = path.node;
303
+ classes = node.classes.map(function(c) {
304
+ return `.${c}`;
305
+ }).join('');
306
+ id = node.id ? `#${node.id}` : '';
307
+ attrStyle = detectStyle(node, o);
308
+ attrs = formatAttributes(node, attrStyle, o);
309
+ header = `${classes}${id}${attrs}`;
310
+ children = (ref = node.children) != null ? ref : [];
311
+ if (children.length === 0) {
312
+ return header;
313
+ }
314
+ if (isSingleInlineChild(children)) {
315
+ childDoc = (ref1 = (ref2 = path.map(printFn, 'children')) != null ? ref2[0] : void 0) != null ? ref1 : '';
316
+ return `${header} ${childDoc}`;
317
+ }
318
+ childDocs = (ref3 = path.map(printFn, 'children')) != null ? ref3 : [];
319
+ joined = joinChildrenWithBlanks(children, childDocs, o);
320
+ return join(hardline, [header, indent(joined)]);
321
+ };
322
+
280
323
  // ─── 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);
324
+ printOutput = function(path, o, printFn) {
325
+ var childDocs, children, expr, joined, marker, node, ref, ref1, ref2;
326
+ node = path.node;
327
+ marker = node.outputKind === 'unescaped' ? '!=' : '=';
328
+ expr = (ref = node.expression.source) != null ? ref : '';
329
+ if (o.coffeeScriptFormat) {
330
+ expr = formatCoffeeScript(expr, o);
331
+ }
332
+ children = (ref1 = node.children) != null ? ref1 : [];
333
+ if (children.length === 0) {
334
+ return `${marker} ${expr}`;
335
+ }
336
+ childDocs = (ref2 = path.map(printFn, 'children')) != null ? ref2 : [];
337
+ joined = joinChildrenWithBlanks(children, childDocs, o);
338
+ return join(hardline, [`${marker} ${expr}`, indent(joined)]);
339
+ };
340
+
341
+ // ─── Control Flow ──────────────────────────────────────────
342
+ printControlFlow = function(path, o, printFn) {
343
+ return printFlow(path, o, printFn);
344
+ };
345
+
346
+ printFlow = function(path, o, printFn) {
347
+ var childLines, children, expr, header, kind, node, ref, ref1, ref2, sep;
348
+ node = path.node;
349
+ kind = node.controlKind;
350
+ // Format expression
351
+ expr = (ref = node.expression.source) != null ? ref : '';
352
+ if (o.coffeeScriptFormat && kind !== 'statement') {
353
+ expr = formatCoffeeScript(expr, o);
354
+ }
355
+ // Build header
356
+ header = (function() {
357
+ switch (kind) {
358
+ case 'statement':
359
+ if (expr) {
360
+ return `- ${expr}`;
361
+ } else {
362
+ return '-';
588
363
  }
589
- else {
590
- const q = o.quoteStyle === 'single' ? "'" : '"';
591
- parts.push(attr.name + '=' + q + attr.value.toString() + q);
364
+ break;
365
+ case 'else':
366
+ return '- else';
367
+ case 'else if':
368
+ return `- else if ${expr}`;
369
+ default:
370
+ return `- ${kind} ${expr}`;
371
+ }
372
+ })();
373
+ children = (ref1 = node.children) != null ? ref1 : [];
374
+ // Inline expansion (controlFlowInline)
375
+ if (o.controlFlowInline && children.length > 0 && (kind === 'if' || kind === 'unless' || kind === 'for' || kind === 'while')) {
376
+ if (children.length === 1 && (children[0] instanceof Element || children[0] instanceof ImplicitDiv)) {
377
+ sep = kind === 'if' || kind === 'unless' ? ' then ' : ' ';
378
+ return `${header}${sep}${printNode(path.map(printFn, 'children.0'), o, printFn)}`;
379
+ }
380
+ }
381
+ if (children.length === 0) {
382
+ return header;
383
+ }
384
+ // Re-parsed merged statements: statement-kind with empty expr + TEXT children
385
+ // Use baked indentation (matching printMergedFlow) to avoid Prettier indent quirks
386
+ if (kind === 'statement' && !expr && children.every(function(c) {
387
+ return c instanceof Text || c instanceof Output;
388
+ })) {
389
+ childLines = children.map(function(c) {
390
+ var val;
391
+ val = (function() {
392
+ var ref2, ref3;
393
+ switch (false) {
394
+ case !(c instanceof Text):
395
+ return c.value;
396
+ case !(c instanceof Output):
397
+ return (ref2 = (ref3 = c.expression) != null ? ref3.source : void 0) != null ? ref2 : '';
398
+ default:
399
+ return '';
592
400
  }
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,
401
+ })();
402
+ return ' ' + val;
610
403
  });
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
404
+ return join(hardline, ['-', childLines.join('\n')]);
405
+ }
406
+ return join(hardline, [header, indent(join(hardline, (ref2 = path.map(printFn, 'children')) != null ? ref2 : []))]);
407
+ };
408
+
409
+ printMergedFlow = function(path, o, printFn) {
410
+ var lines, node;
411
+ node = path.node;
412
+ lines = node.statements.map(function(stmt) {
413
+ var expr, ref;
414
+ expr = (ref = stmt.expression.source) != null ? ref : '';
415
+ if (o.coffeeScriptFormat) {
416
+ expr = formatCoffeeScript(expr, o);
417
+ }
418
+ return ' ' + expr;
419
+ });
420
+ return join(hardline, ['-', lines.join('\n')]);
421
+ };
422
+
423
+ // ─── Comment ───────────────────────────────────────────────
424
+ printComment = function(path, o, _printFn) {
425
+ var node, text;
426
+ node = path.node;
427
+ if (node.commentKind === 'html') {
428
+ return `/ ${node.text}`;
429
+ }
430
+ text = node.text || '';
431
+ if (o.commentFormat && text.length > o.printWidth * 0.7) {
432
+ text = reflowCommentText(text, o.printWidth);
433
+ }
434
+ return `-# ${text}`;
435
+ };
436
+
437
+ reflowCommentText = function(text, width) {
438
+ var current, k, len, lines, word, words;
439
+ words = text.split(/\s+/);
440
+ lines = [];
441
+ current = '';
442
+ for (k = 0, len = words.length; k < len; k++) {
443
+ word = words[k];
444
+ if (current.length + word.length + 1 > width - 3) { // 3 = "-# "
445
+ if (current) {
446
+ lines.push(current.trim());
447
+ }
448
+ current = word;
449
+ } else {
450
+ current += current ? ' ' + word : word;
451
+ }
452
+ }
453
+ if (current) {
454
+ lines.push(current.trim());
455
+ }
456
+ return lines.join('\n-# ');
457
+ };
458
+
459
+ // ─── Filter ────────────────────────────────────────────────
460
+ printFilter = function(path, o, _printFn) {
461
+ var header, indented, node;
462
+ node = path.node;
463
+ header = `:${node.filterName}`;
464
+ if (node.content) {
465
+ indented = node.content.split('\n').map(function(l) {
466
+ return ' ' + l;
467
+ }).join('\n');
468
+ return `${header}\n${indented}`;
469
+ } else {
470
+ return header;
471
+ }
472
+ };
473
+
474
+ // ─── Doctype ───────────────────────────────────────────────
475
+ printDoctype = function(path, o) {
476
+ var node;
477
+ node = path.node;
478
+ return `!!! ${node.value || 'html'}`;
479
+ };
480
+
481
+ // ─── Tag formatting ────────────────────────────────────────
482
+ formatTag = function(tag, o) {
483
+ if (tag instanceof Expression) {
484
+ return tag.source;
485
+ }
486
+ if (o.tagCase === 'lowercase' && /^[a-z]/.test(tag)) {
487
+ return tag.toLowerCase();
488
+ }
489
+ return tag;
490
+ };
491
+
492
+ // ─── Attribute style detection ─────────────────────────────
493
+ detectStyle = function(node, o) {
494
+ var ref;
495
+ if (o.attributeStyle !== 'preserve') {
496
+ return o.attributeStyle;
497
+ }
498
+ return (ref = node.attrStyle) != null ? ref : 'bare';
499
+ };
500
+
501
+ formatAttributes = function(node, style, o) {
502
+ var attr, attrs, expr, k, len, name, parts, ref, ref1, ref2, val;
503
+ attrs = (ref = node.attributes) != null ? ref : [];
504
+ if (!(attrs.length > 0)) {
505
+ return '';
506
+ }
507
+ parts = [];
508
+ for (k = 0, len = attrs.length; k < len; k++) {
509
+ attr = attrs[k];
510
+ if (attr.spread) {
511
+ expr = (ref1 = attr.expression.source) != null ? ref1 : '';
512
+ if (o.coffeeScriptFormat) {
513
+ expr = formatCoffeeScript(expr, o);
514
+ }
515
+ parts.push(style === 'parens' ? `...${expr}` : `{${expr}...}`);
516
+ continue;
517
+ }
518
+ name = attr.name;
519
+ val = (ref2 = attr.value.source) != null ? ref2 : '';
520
+ if (o.coffeeScriptFormat) {
521
+ val = formatCoffeeScript(val, o);
522
+ }
523
+ if (attr.shorthand) {
524
+ parts.push(name);
525
+ } else {
526
+ parts.push(`${name}: ${val}`);
527
+ }
528
+ }
529
+ switch (style) {
530
+ case 'braces':
531
+ return `{ ${parts.join(', ')} }`;
532
+ case 'parens':
533
+ return `( ${parts.join(', ')} )`;
534
+ default:
535
+ return parts.join(', ');
536
+ }
537
+ };
538
+
539
+ // ─── CoffeeScript Embed (for Prettier embed) ───────────────
540
+ export var embedCoffeeScript = function(path, _print) {
541
+ var expr, node, ref;
542
+ node = path.node;
543
+ if (!node) {
544
+ return null;
545
+ }
546
+ if (node instanceof Output || node instanceof ControlFlow) {
547
+ expr = (ref = node.expression) != null ? ref.source : void 0;
548
+ if (!(expr && /[\u0000-\u00ff]/.test(expr))) {
549
+ return null;
550
+ }
551
+ null;
552
+ }
553
+ return null;
554
+ };
555
+
556
+ export {
557
+ MergedStatements,
558
+ mergeAllStatements
559
+ };