coffeehaml 0.7.0 → 0.7.2

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 (62) 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/types.js +122 -12
  16. package/dist/vite-plugin.js +78 -63
  17. package/package.json +4 -9
  18. package/dist/ast.d.ts +0 -123
  19. package/dist/ast.d.ts.map +0 -1
  20. package/dist/ast.js.map +0 -1
  21. package/dist/cli.d.ts +0 -3
  22. package/dist/cli.d.ts.map +0 -1
  23. package/dist/cli.js.map +0 -1
  24. package/dist/compiler.d.ts +0 -10
  25. package/dist/compiler.d.ts.map +0 -1
  26. package/dist/compiler.js.map +0 -1
  27. package/dist/emitter.d.ts +0 -4
  28. package/dist/emitter.d.ts.map +0 -1
  29. package/dist/emitter.js.map +0 -1
  30. package/dist/errors.d.ts +0 -3
  31. package/dist/errors.d.ts.map +0 -1
  32. package/dist/errors.js.map +0 -1
  33. package/dist/expressions.d.ts +0 -22
  34. package/dist/expressions.d.ts.map +0 -1
  35. package/dist/expressions.js.map +0 -1
  36. package/dist/index.d.ts +0 -7
  37. package/dist/index.d.ts.map +0 -1
  38. package/dist/index.js.map +0 -1
  39. package/dist/lexer.d.ts +0 -30
  40. package/dist/lexer.d.ts.map +0 -1
  41. package/dist/lexer.js.map +0 -1
  42. package/dist/parser.d.ts +0 -10
  43. package/dist/parser.d.ts.map +0 -1
  44. package/dist/parser.js.map +0 -1
  45. package/dist/prettier/coffeescript-formatter.d.ts +0 -31
  46. package/dist/prettier/coffeescript-formatter.d.ts.map +0 -1
  47. package/dist/prettier/coffeescript-formatter.js.map +0 -1
  48. package/dist/prettier/index.d.ts +0 -8
  49. package/dist/prettier/index.d.ts.map +0 -1
  50. package/dist/prettier/index.js.map +0 -1
  51. package/dist/prettier/options.d.ts +0 -10
  52. package/dist/prettier/options.d.ts.map +0 -1
  53. package/dist/prettier/options.js.map +0 -1
  54. package/dist/prettier/printer.d.ts +0 -45
  55. package/dist/prettier/printer.d.ts.map +0 -1
  56. package/dist/prettier/printer.js.map +0 -1
  57. package/dist/types.d.ts +0 -76
  58. package/dist/types.d.ts.map +0 -1
  59. package/dist/types.js.map +0 -1
  60. package/dist/vite-plugin.d.ts +0 -7
  61. package/dist/vite-plugin.d.ts.map +0 -1
  62. package/dist/vite-plugin.js.map +0 -1
package/dist/parser.js CHANGED
@@ -1,480 +1,575 @@
1
- import { TokenType } from './lexer.js';
2
- import { Document, Element, ImplicitDiv, Text, Output, ControlFlow, Comment, Filter, Doctype, Expression, } from './ast.js';
3
- import { CompileError } from './types.js';
4
- // ─── Parser State ──────────────────────────────────────────
5
- class ParserState {
6
- constructor(tokens, filename) {
7
- this.errors = [];
8
- this.tokens = tokens;
9
- this.pos = 0;
10
- this.filename = filename;
1
+ // Generated by CoffeeScript 2.7.0
2
+ // parser.coffee — recursive descent parser
3
+ // Converts token stream into AST
4
+ var ParserState, findColon, isElse, parseAttributeBlock, parseBlock, parseComment, parseControlFlow, parseControlKind, parseDoctype, parseElement, parseFilter, parseHtmlComment, parseImplicitDiv, parseNode, parseOutput, parseText, splitAttributePairs, stripControlKeyword;
5
+
6
+ import {
7
+ TokenType
8
+ } from './lexer.js';
9
+
10
+ import {
11
+ Document,
12
+ Element,
13
+ ImplicitDiv,
14
+ Text,
15
+ Output,
16
+ ControlFlow,
17
+ Comment,
18
+ Filter,
19
+ Doctype,
20
+ Expression,
21
+ Attribute,
22
+ SpreadAttribute
23
+ } from './ast.js';
24
+
25
+ import {
26
+ CompileError
27
+ } from './types.js';
28
+
29
+ ParserState = (function() {
30
+ // ─── Parser State ──────────────────────────────────────────
31
+ class ParserState {
32
+ constructor(tokens1, filename1 = null) {
33
+ this.tokens = tokens1;
34
+ this.filename = filename1;
35
+ this.pos = 0;
36
+ this.errors = [];
11
37
  }
12
- get current() {
13
- return this.pos < this.tokens.length ? this.tokens[this.pos] : null;
38
+
39
+ current() {
40
+ if (this.pos < this.tokens.length) {
41
+ return this.tokens[this.pos];
42
+ } else {
43
+ return null;
44
+ }
14
45
  }
15
- get peek() {
16
- return this.pos + 1 < this.tokens.length ? this.tokens[this.pos + 1] : null;
46
+
47
+ peek() {
48
+ if (this.pos + 1 < this.tokens.length) {
49
+ return this.tokens[this.pos + 1];
50
+ } else {
51
+ return null;
52
+ }
17
53
  }
54
+
18
55
  advance() {
19
- const token = this.current;
20
- this.pos++;
21
- return token;
56
+ var token;
57
+ token = this.current();
58
+ this.pos++;
59
+ return token;
22
60
  }
23
- /** Record an error and attempt recovery by skipping to the next safe boundary. */
61
+
24
62
  error(message, code = 'PARSE_ERROR') {
25
- this.errors.push(new CompileError(message, 'parser', code, this.current?.location));
63
+ var ref;
64
+ return this.errors.push(new CompileError(message, 'parser', code, (ref = this.current()) != null ? ref.location : void 0));
26
65
  }
27
- /** Expect a token type. On mismatch, records error and recovers (never throws). */
66
+
28
67
  expect(type) {
29
- const token = this.current;
30
- if (!token || token.type !== type) {
31
- this.error(`Expected ${type} but got ${token?.type ?? 'EOF'}`, 'UNEXPECTED_TOKEN');
32
- // Recovery: skip to next structural token
33
- this.recover();
34
- return null;
35
- }
36
- return this.advance();
68
+ var ref, token;
69
+ token = this.current();
70
+ if (!token || token.type !== type) {
71
+ this.error(`Expected ${type} but got ${(ref = token != null ? token.type : void 0) != null ? ref : 'EOF'}`, 'UNEXPECTED_TOKEN');
72
+ this.recover();
73
+ return null;
74
+ }
75
+ return this.advance();
37
76
  }
38
- /** Skip tokens until a structural boundary (INDENT/DEDENT/block-level token). */
77
+
39
78
  recover() {
40
- while (this.current) {
41
- const t = this.current.type;
42
- if (t === TokenType.TAG || t === TokenType.CLASS || t === TokenType.ID ||
43
- t === TokenType.OUTPUT || t === TokenType.OUTPUT_UNESC || t === TokenType.CONTROL ||
44
- t === TokenType.FILTER || t === TokenType.COMMENT || t === TokenType.HTML_COMMENT ||
45
- t === TokenType.DOCTYPE || t === TokenType.INDENT || t === TokenType.DEDENT) {
46
- return;
47
- }
48
- this.advance();
79
+ var t;
80
+ while (this.current()) {
81
+ t = this.current().type;
82
+ if (t === TokenType.TAG || t === TokenType.CLASS || t === TokenType.ID || t === TokenType.OUTPUT || t === TokenType.OUTPUT_UNESC || t === TokenType.CONTROL || t === TokenType.FILTER || t === TokenType.COMMENT || t === TokenType.HTML_COMMENT || t === TokenType.DOCTYPE || t === TokenType.INDENT || t === TokenType.DEDENT) {
83
+ return;
49
84
  }
85
+ this.advance();
86
+ }
50
87
  }
88
+
51
89
  skip(type) {
52
- if (this.current?.type === type) {
53
- this.advance();
54
- return true;
55
- }
56
- return false;
90
+ var ref;
91
+ if (((ref = this.current()) != null ? ref.type : void 0) === type) {
92
+ this.advance();
93
+ return true;
94
+ }
95
+ return false;
57
96
  }
97
+
58
98
  isAt(type) {
59
- return this.current?.type === type;
99
+ var ref;
100
+ return ((ref = this.current()) != null ? ref.type : void 0) === type;
60
101
  }
61
- }
102
+
103
+ };
104
+
105
+ ParserState.prototype.tokens = [];
106
+
107
+ ParserState.prototype.pos = 0;
108
+
109
+ ParserState.prototype.filename = null;
110
+
111
+ ParserState.prototype.errors = [];
112
+
113
+ return ParserState;
114
+
115
+ }).call(this);
116
+
62
117
  // ─── Public API ────────────────────────────────────────────
63
- export function parse(tokens, filename) {
64
- const state = new ParserState(tokens, filename);
65
- // Collect prologue (raw JS lines before the first HAML construct)
66
- const prologue = [];
67
- while (state.current?.type === TokenType.PROLOGUE) {
68
- prologue.push(state.advance().value);
69
- }
70
- const children = parseBlock(state);
71
- return { document: new Document(children, prologue), errors: state.errors };
72
- }
118
+ export var parse = function(tokens, filename = null) {
119
+ var children, prologue, ref, state;
120
+ state = new ParserState(tokens, filename);
121
+ prologue = [];
122
+ while (((ref = state.current()) != null ? ref.type : void 0) === TokenType.PROLOGUE) {
123
+ prologue.push(state.advance().value);
124
+ }
125
+ children = parseBlock(state);
126
+ return {
127
+ document: new Document(children, prologue),
128
+ errors: state.errors
129
+ };
130
+ };
131
+
73
132
  // ─── Block Parsing ─────────────────────────────────────────
74
- /** Parse nodes until DEDENT or EOF. */
75
- function parseBlock(state) {
76
- const nodes = [];
77
- while (state.current && state.current.type !== TokenType.DEDENT) {
78
- const node = parseNode(state);
79
- if (node)
80
- nodes.push(node);
133
+ parseBlock = function(state) {
134
+ var node, nodes;
135
+ nodes = [];
136
+ while (state.current() && state.current().type !== TokenType.DEDENT) {
137
+ node = parseNode(state);
138
+ if (node) {
139
+ nodes.push(node);
81
140
  }
82
- return nodes;
83
- }
141
+ }
142
+ return nodes;
143
+ };
144
+
84
145
  // ─── Node Dispatcher ───────────────────────────────────────
85
- function parseNode(state) {
86
- const token = state.current;
87
- if (!token)
88
- return null;
89
- switch (token.type) {
90
- case TokenType.TAG:
91
- return parseElement(state);
92
- case TokenType.CLASS:
93
- case TokenType.ID:
94
- return parseImplicitDiv(state);
95
- case TokenType.OUTPUT:
96
- case TokenType.OUTPUT_UNESC:
97
- return parseOutput(state);
98
- case TokenType.CONTROL:
99
- return parseControlFlow(state);
100
- case TokenType.COMMENT:
101
- return parseComment(state);
102
- case TokenType.HTML_COMMENT:
103
- return parseHtmlComment(state);
104
- case TokenType.FILTER:
105
- return parseFilter(state);
106
- case TokenType.DOCTYPE:
107
- return parseDoctype(state);
108
- case TokenType.TEXT:
109
- return parseText(state);
110
- case TokenType.INDENT:
111
- state.advance(); // skip, handled by parseBlock
112
- return null;
113
- case TokenType.DEDENT:
114
- return null; // handled by parseBlock loop
115
- case TokenType.NEWLINE:
116
- state.advance();
117
- return null;
118
- default:
119
- state.advance();
120
- return null;
121
- }
122
- }
146
+ parseNode = function(state) {
147
+ var token;
148
+ token = state.current();
149
+ if (!token) {
150
+ return null;
151
+ }
152
+ switch (token.type) {
153
+ case TokenType.TAG:
154
+ return parseElement(state);
155
+ case TokenType.CLASS:
156
+ return parseImplicitDiv(state);
157
+ case TokenType.ID:
158
+ return parseImplicitDiv(state);
159
+ case TokenType.OUTPUT:
160
+ return parseOutput(state);
161
+ case TokenType.OUTPUT_UNESC:
162
+ return parseOutput(state);
163
+ case TokenType.CONTROL:
164
+ return parseControlFlow(state);
165
+ case TokenType.COMMENT:
166
+ return parseComment(state);
167
+ case TokenType.HTML_COMMENT:
168
+ return parseHtmlComment(state);
169
+ case TokenType.FILTER:
170
+ return parseFilter(state);
171
+ case TokenType.DOCTYPE:
172
+ return parseDoctype(state);
173
+ case TokenType.TEXT:
174
+ return parseText(state);
175
+ case TokenType.INDENT:
176
+ state.advance();
177
+ return null;
178
+ case TokenType.DEDENT:
179
+ return null;
180
+ case TokenType.NEWLINE:
181
+ state.advance();
182
+ return null;
183
+ default:
184
+ state.advance();
185
+ return null;
186
+ }
187
+ };
188
+
123
189
  // ─── Element ───────────────────────────────────────────────
124
- function parseElement(state) {
125
- const tagToken = state.expect(TokenType.TAG);
126
- if (!tagToken)
127
- return new Element('div', { location: state.current?.location });
128
- const tag = tagToken.value;
129
- const isComponent = /^[A-Z]/.test(tag);
130
- let classes = [];
131
- let id = null;
132
- let attributes = [];
133
- let isSelfClosing = false;
134
- let attrStyle = null;
135
- // Parse modifiers and attributes
136
- while (state.current &&
137
- (state.current.type === TokenType.CLASS ||
138
- state.current.type === TokenType.ID ||
139
- state.current.type === TokenType.ATTRS_BRACE ||
140
- state.current.type === TokenType.ATTRS_PAREN ||
141
- state.current.type === TokenType.SELF_CLOSE)) {
142
- const tok = state.current;
143
- if (tok.type === TokenType.CLASS) {
144
- state.advance();
145
- classes.push(tok.value);
146
- }
147
- else if (tok.type === TokenType.ID) {
148
- state.advance();
149
- id = tok.value; // last #id wins
150
- }
151
- else if (tok.type === TokenType.ATTRS_BRACE) {
152
- state.advance();
153
- attributes.push(...parseAttributeBlock(tok.value, '{}', tagToken.location));
154
- attrStyle = 'braces';
155
- }
156
- else if (tok.type === TokenType.ATTRS_PAREN) {
157
- state.advance();
158
- attributes.push(...parseAttributeBlock(tok.value, '()', tagToken.location));
159
- attrStyle = 'parens';
160
- }
161
- else if (tok.type === TokenType.SELF_CLOSE) {
162
- state.advance();
163
- isSelfClosing = true;
164
- }
165
- }
166
- // Parse inline text or output if present
167
- let children = [];
168
- if (state.current && !isSelfClosing) {
169
- if (state.current.type === TokenType.OUTPUT) {
170
- const tok = state.advance();
171
- children.push(new Output(new Expression(tok.value), 'escaped', tok.location));
172
- }
173
- else if (state.current.type === TokenType.OUTPUT_UNESC) {
174
- const tok = state.advance();
175
- children.push(new Output(new Expression(tok.value), 'unescaped', tok.location));
176
- }
177
- else if (state.current.type === TokenType.TEXT) {
178
- const textToken = state.advance();
179
- children.push(new Text(textToken.value, textToken.location));
180
- }
181
- }
182
- // Parse child block if INDENT follows
183
- if (state.current?.type === TokenType.INDENT && !isSelfClosing) {
184
- state.advance(); // consume INDENT
185
- children = children.concat(parseBlock(state));
186
- state.expect(TokenType.DEDENT);
187
- }
188
- return new Element(tag, { classes, id, attributes, children, isComponent, isSelfClosing, attrStyle, location: tagToken.location });
189
- }
190
- // ─── ImplicitDiv ───────────────────────────────────────────
191
- function parseImplicitDiv(state) {
192
- let classes = [];
193
- let id = null;
194
- let attributes = [];
195
- let attrStyle = null;
196
- const firstToken = state.current; // for location
197
- while (state.current &&
198
- (state.current.type === TokenType.CLASS ||
199
- state.current.type === TokenType.ID ||
200
- state.current.type === TokenType.ATTRS_BRACE ||
201
- state.current.type === TokenType.ATTRS_PAREN)) {
202
- const tok = state.current;
203
- if (tok.type === TokenType.CLASS) {
204
- state.advance();
205
- classes.push(tok.value);
206
- }
207
- else if (tok.type === TokenType.ID) {
208
- state.advance();
209
- id = tok.value;
210
- }
211
- else if (tok.type === TokenType.ATTRS_BRACE) {
212
- state.advance();
213
- attributes.push(...parseAttributeBlock(tok.value, '{}', firstToken.location));
214
- attrStyle = 'braces';
215
- }
216
- else if (tok.type === TokenType.ATTRS_PAREN) {
217
- state.advance();
218
- attributes.push(...parseAttributeBlock(tok.value, '()', firstToken.location));
219
- attrStyle = 'parens';
220
- }
190
+ parseElement = function(state) {
191
+ var attrStyle, attributes, children, classes, id, isComponent, isSelfClosing, ref, ref1, ref2, tag, tagToken, textToken, tok;
192
+ tagToken = state.expect(TokenType.TAG);
193
+ if (!tagToken) {
194
+ return new Element('div', {
195
+ location: (ref = state.current()) != null ? ref.location : void 0
196
+ });
197
+ }
198
+ tag = tagToken.value;
199
+ isComponent = /^[A-Z]/.test(tag);
200
+ classes = [];
201
+ id = null;
202
+ attributes = [];
203
+ isSelfClosing = false;
204
+ attrStyle = null;
205
+ while (state.current() && ((ref1 = state.current().type) === TokenType.CLASS || ref1 === TokenType.ID || ref1 === TokenType.ATTRS_BRACE || ref1 === TokenType.ATTRS_PAREN || ref1 === TokenType.SELF_CLOSE)) {
206
+ tok = state.current();
207
+ switch (tok.type) {
208
+ case TokenType.CLASS:
209
+ state.advance();
210
+ classes.push(tok.value);
211
+ break;
212
+ case TokenType.ID:
213
+ state.advance();
214
+ id = tok.value;
215
+ break;
216
+ case TokenType.ATTRS_BRACE:
217
+ state.advance();
218
+ attributes.push(...parseAttributeBlock(tok.value, '{}', tagToken.location));
219
+ attrStyle = 'braces';
220
+ break;
221
+ case TokenType.ATTRS_PAREN:
222
+ state.advance();
223
+ attributes.push(...parseAttributeBlock(tok.value, '()', tagToken.location));
224
+ attrStyle = 'parens';
225
+ break;
226
+ case TokenType.SELF_CLOSE:
227
+ state.advance();
228
+ isSelfClosing = true;
221
229
  }
222
- // Parse inline text
223
- let children = [];
224
- if (state.current && state.current.type === TokenType.TEXT) {
225
- const textToken = state.advance();
230
+ }
231
+ // Parse inline text or output
232
+ children = [];
233
+ if (state.current() && !isSelfClosing) {
234
+ switch (state.current().type) {
235
+ case TokenType.OUTPUT:
236
+ tok = state.advance();
237
+ children.push(new Output(new Expression(tok.value), 'escaped', tok.location));
238
+ break;
239
+ case TokenType.OUTPUT_UNESC:
240
+ tok = state.advance();
241
+ children.push(new Output(new Expression(tok.value), 'unescaped', tok.location));
242
+ break;
243
+ case TokenType.TEXT:
244
+ textToken = state.advance();
226
245
  children.push(new Text(textToken.value, textToken.location));
227
246
  }
228
- // Parse child block
229
- if (state.current?.type === TokenType.INDENT) {
247
+ }
248
+ // Parse child block if INDENT follows
249
+ if (((ref2 = state.current()) != null ? ref2.type : void 0) === TokenType.INDENT && !isSelfClosing) {
250
+ state.advance();
251
+ children = children.concat(parseBlock(state));
252
+ state.expect(TokenType.DEDENT);
253
+ }
254
+ return new Element(tag, {
255
+ classes,
256
+ id,
257
+ attributes,
258
+ children,
259
+ isComponent,
260
+ isSelfClosing,
261
+ attrStyle,
262
+ location: tagToken.location
263
+ });
264
+ };
265
+
266
+ // ─── ImplicitDiv ───────────────────────────────────────────
267
+ parseImplicitDiv = function(state) {
268
+ var attrStyle, attributes, children, classes, firstToken, id, ref, ref1, ref2, textToken, tok;
269
+ classes = [];
270
+ id = null;
271
+ attributes = [];
272
+ attrStyle = null;
273
+ firstToken = state.current();
274
+ while (state.current() && ((ref = state.current().type) === TokenType.CLASS || ref === TokenType.ID || ref === TokenType.ATTRS_BRACE || ref === TokenType.ATTRS_PAREN)) {
275
+ tok = state.current();
276
+ switch (tok.type) {
277
+ case TokenType.CLASS:
230
278
  state.advance();
231
- children = children.concat(parseBlock(state));
232
- state.expect(TokenType.DEDENT);
279
+ classes.push(tok.value);
280
+ break;
281
+ case TokenType.ID:
282
+ state.advance();
283
+ id = tok.value;
284
+ break;
285
+ case TokenType.ATTRS_BRACE:
286
+ state.advance();
287
+ attributes.push(...parseAttributeBlock(tok.value, '{}', firstToken.location));
288
+ attrStyle = 'braces';
289
+ break;
290
+ case TokenType.ATTRS_PAREN:
291
+ state.advance();
292
+ attributes.push(...parseAttributeBlock(tok.value, '()', firstToken.location));
293
+ attrStyle = 'parens';
233
294
  }
234
- return new ImplicitDiv({ classes, id, attributes, children, attrStyle, location: firstToken.location });
235
- }
295
+ }
296
+ // Parse inline text
297
+ children = [];
298
+ if (((ref1 = state.current()) != null ? ref1.type : void 0) === TokenType.TEXT) {
299
+ textToken = state.advance();
300
+ children.push(new Text(textToken.value, textToken.location));
301
+ }
302
+ // Parse child block
303
+ if (((ref2 = state.current()) != null ? ref2.type : void 0) === TokenType.INDENT) {
304
+ state.advance();
305
+ children = children.concat(parseBlock(state));
306
+ state.expect(TokenType.DEDENT);
307
+ }
308
+ return new ImplicitDiv({
309
+ classes,
310
+ id,
311
+ attributes,
312
+ children,
313
+ attrStyle,
314
+ location: firstToken.location
315
+ });
316
+ };
317
+
236
318
  // ─── Output ────────────────────────────────────────────────
237
- function parseOutput(state) {
238
- const token = state.current;
239
- const outputKind = token.type === TokenType.OUTPUT ? 'escaped' : 'unescaped';
319
+ parseOutput = function(state) {
320
+ var children, expr, outputKind, ref, token;
321
+ token = state.current();
322
+ outputKind = token.type === TokenType.OUTPUT ? 'escaped' : 'unescaped';
323
+ state.advance();
324
+ expr = new Expression(token.value);
325
+ children = [];
326
+ if (((ref = state.current()) != null ? ref.type : void 0) === TokenType.INDENT) {
240
327
  state.advance();
241
- const expr = new Expression(token.value, undefined);
242
- // Parse continuation children if INDENT follows
243
- let children = [];
244
- if (state.current?.type === TokenType.INDENT) {
245
- state.advance();
246
- children = parseBlock(state);
247
- state.expect(TokenType.DEDENT);
248
- }
249
- return new Output(expr, outputKind, token.location, children);
250
- }
328
+ children = parseBlock(state);
329
+ state.expect(TokenType.DEDENT);
330
+ }
331
+ return new Output(expr, outputKind, token.location, children);
332
+ };
333
+
251
334
  // ─── Control Flow ──────────────────────────────────────────
252
- function parseControlFlow(state) {
253
- const token = state.expect(TokenType.CONTROL);
254
- if (!token)
255
- return new ControlFlow('statement', new Expression(''), [], null, state.current?.location);
256
- let source = token.value;
257
- // Normalize "else if" → "if" so it chains naturally in ternary
258
- if (/^\s*else\s+if\b/.test(source)) {
259
- source = source.replace(/^\s*else\s+/, '');
335
+ parseControlFlow = function(state) {
336
+ var children, controlKind, expr, exprSource, next, nextSource, ref, ref1, ref2, source, token;
337
+ token = state.expect(TokenType.CONTROL);
338
+ if (!token) {
339
+ return new ControlFlow('statement', new Expression(''), [], null, (ref = state.current()) != null ? ref.location : void 0);
340
+ }
341
+ source = token.value;
342
+ // Normalize "else if" → "if"
343
+ if (/^\s*else\s+if\b/.test(source)) {
344
+ source = source.replace(/^\s*else\s+/, '');
345
+ }
346
+ controlKind = parseControlKind(source);
347
+ exprSource = stripControlKeyword(source, controlKind);
348
+ expr = new Expression(exprSource);
349
+ children = [];
350
+ if (((ref1 = state.current()) != null ? ref1.type : void 0) === TokenType.INDENT) {
351
+ state.advance();
352
+ children = parseBlock(state);
353
+ state.expect(TokenType.DEDENT);
354
+ }
355
+ // Check for chained else / else if
356
+ next = null;
357
+ if (((ref2 = state.current()) != null ? ref2.type : void 0) === TokenType.CONTROL) {
358
+ nextSource = state.current().value.trimStart();
359
+ if (isElse(nextSource)) {
360
+ next = parseControlFlow(state);
260
361
  }
261
- // Parse the control kind from the expression
262
- const controlKind = parseControlKind(source);
263
- // Strip the keyword from the expression — store only the condition/iterable
264
- const exprSource = stripControlKeyword(source, controlKind);
265
- const expr = new Expression(exprSource, undefined);
266
- // Parse body if INDENT follows
267
- let children = [];
268
- if (state.current?.type === TokenType.INDENT) {
362
+ }
363
+ return new ControlFlow(controlKind, expr, children, next, token.location);
364
+ };
365
+
366
+ parseControlKind = function(source) {
367
+ var keyword;
368
+ keyword = source.trimStart().split(/\s+/)[0];
369
+ switch (keyword) {
370
+ case 'if':
371
+ return 'if';
372
+ case 'unless':
373
+ return 'unless';
374
+ case 'for':
375
+ return 'for';
376
+ case 'while':
377
+ return 'while';
378
+ case 'else':
379
+ return 'else';
380
+ default:
381
+ return 'statement';
382
+ }
383
+ };
384
+
385
+ stripControlKeyword = function(source, kind) {
386
+ var keyword, re, trimmed;
387
+ trimmed = source.trimStart();
388
+ if (kind === 'statement') {
389
+ return trimmed;
390
+ }
391
+ keyword = kind === 'else' ? 'else' : kind;
392
+ re = new RegExp(`^\\s*${keyword}\\b\\s*`);
393
+ return trimmed.replace(re, '');
394
+ };
395
+
396
+ isElse = function(source) {
397
+ return /^\s*else\b/.test(source);
398
+ };
399
+
400
+ // ─── Comments ──────────────────────────────────────────────
401
+ parseComment = function(state) {
402
+ var token;
403
+ token = state.expect(TokenType.COMMENT);
404
+ if (!token) {
405
+ return new Comment('haml', '');
406
+ }
407
+ return new Comment('haml', token.value, token.location);
408
+ };
409
+
410
+ parseHtmlComment = function(state) {
411
+ var token;
412
+ token = state.expect(TokenType.HTML_COMMENT);
413
+ if (!token) {
414
+ return new Comment('html', '');
415
+ }
416
+ return new Comment('html', token.value, token.location);
417
+ };
418
+
419
+ // ─── Filter ────────────────────────────────────────────────
420
+ parseFilter = function(state) {
421
+ var content, filterName, lines, parts, tok, token;
422
+ token = state.expect(TokenType.FILTER);
423
+ if (!token) {
424
+ return new Filter('', '');
425
+ }
426
+ parts = token.value.split('\n');
427
+ filterName = parts[0];
428
+ content = parts.slice(1).join('\n');
429
+ // Consume indented body
430
+ if (state.isAt(TokenType.INDENT)) {
431
+ state.advance();
432
+ lines = [];
433
+ while (state.current() && !state.isAt(TokenType.DEDENT)) {
434
+ tok = state.current();
435
+ if (tok.type === TokenType.TEXT) {
436
+ lines.push(tok.value);
269
437
  state.advance();
270
- children = parseBlock(state);
271
- state.expect(TokenType.DEDENT);
272
- }
273
- // Check for chained else / else if
274
- let next = null;
275
- if (state.current?.type === TokenType.CONTROL) {
276
- const nextSource = state.current.value.trimStart();
277
- if (isElse(nextSource)) {
278
- next = parseControlFlow(state);
279
- }
280
- }
281
- return new ControlFlow(controlKind, expr, children, next, token.location);
282
- }
283
- function parseControlKind(source) {
284
- const trimmed = source.trimStart();
285
- const keyword = trimmed.split(/\s+/)[0];
286
- switch (keyword) {
287
- case 'if': return 'if';
288
- case 'unless': return 'unless';
289
- case 'for': return 'for';
290
- case 'while': return 'while';
291
- case 'else': return 'else';
292
- default: return 'statement';
438
+ } else if (tok.type === TokenType.NEWLINE) {
439
+ state.advance();
440
+ } else {
441
+ break;
442
+ }
293
443
  }
294
- }
295
- function stripControlKeyword(source, kind) {
296
- const trimmed = source.trimStart();
297
- if (kind === 'statement') {
298
- return trimmed; // no keyword to strip — entire source is the statement
444
+ if (state.isAt(TokenType.DEDENT)) {
445
+ state.advance();
299
446
  }
300
- // Remove the keyword and any whitespace after it
301
- const keyword = kind === 'else' ? 'else' : kind;
302
- const re = new RegExp("^\\s*" + keyword + "\\b\\s*");
303
- return trimmed.replace(re, '');
304
- }
305
- function isElse(source) {
306
- return /^\s*else\b/.test(source);
307
- }
308
- // ─── Comments ──────────────────────────────────────────────
309
- function parseComment(state) {
310
- const token = state.expect(TokenType.COMMENT);
311
- if (!token)
312
- return new Comment('haml', '');
313
- return new Comment('haml', token.value, token.location);
314
- }
315
- function parseHtmlComment(state) {
316
- const token = state.expect(TokenType.HTML_COMMENT);
317
- if (!token)
318
- return new Comment('html', '');
319
- return new Comment('html', token.value, token.location);
320
- }
321
- // ─── Filter ────────────────────────────────────────────────
322
- function parseFilter(state) {
323
- const token = state.expect(TokenType.FILTER);
324
- if (!token)
325
- return new Filter('', '');
326
- const parts = token.value.split('\n');
327
- const filterName = parts[0];
328
- let content = parts.slice(1).join('\n');
329
- // Consume indented children as filter body (Haml convention).
330
- // After `:markdown`, all indented content belongs to the filter.
331
- if (state.isAt(TokenType.INDENT)) {
332
- state.advance(); // skip INDENT
333
- const lines = [];
334
- while (state.current && !state.isAt(TokenType.DEDENT)) {
335
- const tok = state.current;
336
- if (tok.type === TokenType.TEXT) {
337
- lines.push(tok.value);
338
- state.advance();
339
- }
340
- else if (tok.type === TokenType.NEWLINE) {
341
- state.advance();
342
- }
343
- else {
344
- // Unexpected token in filter body — break to avoid infinite loop
345
- break;
346
- }
347
- }
348
- // Consume DEDENT
349
- if (state.isAt(TokenType.DEDENT)) {
350
- state.advance();
351
- }
352
- if (lines.length > 0) {
353
- content = (content ? content + '\n' : '') + lines.join('\n');
354
- }
447
+ if (lines.length > 0) {
448
+ content = content ? content + '\n' + lines.join('\n') : lines.join('\n');
355
449
  }
356
- return new Filter(filterName, content, token.location);
357
- }
450
+ }
451
+ return new Filter(filterName, content, token.location);
452
+ };
453
+
358
454
  // ─── Doctype ───────────────────────────────────────────────
359
- function parseDoctype(state) {
360
- const token = state.expect(TokenType.DOCTYPE);
361
- if (!token)
362
- return new Doctype('html');
363
- return new Doctype(token.value || 'html', token.location);
364
- }
455
+ parseDoctype = function(state) {
456
+ var token;
457
+ token = state.expect(TokenType.DOCTYPE);
458
+ if (!token) {
459
+ return new Doctype('html');
460
+ }
461
+ return new Doctype(token.value || 'html', token.location);
462
+ };
463
+
365
464
  // ─── Text ──────────────────────────────────────────────────
366
- function parseText(state) {
367
- const token = state.expect(TokenType.TEXT);
368
- if (!token)
369
- return new Text('');
370
- return new Text(token.value, token.location);
371
- }
465
+ parseText = function(state) {
466
+ var token;
467
+ token = state.expect(TokenType.TEXT);
468
+ if (!token) {
469
+ return new Text('');
470
+ }
471
+ return new Text(token.value, token.location);
472
+ };
473
+
372
474
  // ─── Attribute Block Parser ────────────────────────────────
373
- /** Parse a CoffeeScript object literal into attributes.
374
- * Uses a simple key-value parser that handles:
375
- * {key: value, key2: value2}
376
- * {shorthand}
377
- * {key: "string", 'quoted-key': val, nested: {a: 1}}
378
- */
379
- function parseAttributeBlock(source, _style, _location) {
380
- const attrs = [];
381
- if (!source.trim())
382
- return attrs;
383
- // Simple attribute parser: split on commas outside of brackets/strings
384
- const pairs = splitAttributePairs(source);
385
- for (const pair of pairs) {
386
- const trimmed = pair.trim();
387
- if (!trimmed)
388
- continue;
389
- // Spread attribute: props... (CoffeeScript form) or ...props (JSX form)
390
- if (trimmed.endsWith('...')) {
391
- const expr = trimmed.slice(0, -3).trim();
392
- if (expr) {
393
- attrs.push({ spread: true, expression: new Expression(expr) });
394
- }
395
- continue;
396
- }
397
- if (trimmed.startsWith('...')) {
398
- const expr = trimmed.slice(3).trim();
399
- if (expr) {
400
- attrs.push({ spread: true, expression: new Expression(expr) });
401
- }
402
- continue;
403
- }
404
- const colonIdx = findColon(trimmed);
405
- if (colonIdx === -1) {
406
- // Shorthand: {foo} → foo={foo}
407
- attrs.push({
408
- name: trimmed,
409
- value: new Expression(trimmed),
410
- shorthand: true,
411
- });
412
- }
413
- else {
414
- const name = trimmed.slice(0, colonIdx).trim();
415
- const value = trimmed.slice(colonIdx + 1).trim();
416
- // Strip quotes from key if present
417
- const cleanName = name.replace(/^['"]|['"]$/g, '');
418
- attrs.push({
419
- name: cleanName,
420
- value: new Expression(value),
421
- shorthand: false,
422
- });
423
- }
424
- }
475
+ parseAttributeBlock = function(source, _style, _location = null) {
476
+ var attrs, cleanName, colonIdx, expr, j, len, name, pair, pairs, trimmed, value;
477
+ attrs = [];
478
+ if (!source.trim()) {
425
479
  return attrs;
426
- }
427
- /** Split attribute string on commas, respecting nested brackets and string literals. */
428
- function splitAttributePairs(source) {
429
- const result = [];
430
- let depth = 0;
431
- let start = 0;
432
- for (let i = 0; i < source.length; i++) {
433
- const ch = source[i];
434
- if (ch === '{' || ch === '[' || ch === '(')
435
- depth++;
436
- else if (ch === '}' || ch === ']' || ch === ')')
437
- depth--;
438
- else if ((ch === '"' || ch === "'") && depth === 0) {
439
- // Skip over string literals
440
- const q = ch;
441
- i++;
442
- while (i < source.length && source[i] !== q) {
443
- if (source[i] === '\\')
444
- i++;
445
- i++;
446
- }
447
- }
448
- else if (ch === ',' && depth === 0) {
449
- result.push(source.slice(start, i));
450
- start = i + 1;
451
- }
480
+ }
481
+ pairs = splitAttributePairs(source);
482
+ for (j = 0, len = pairs.length; j < len; j++) {
483
+ pair = pairs[j];
484
+ trimmed = pair.trim();
485
+ if (!trimmed) {
486
+ continue;
487
+ }
488
+ // Spread: props... or ...props
489
+ if (trimmed.endsWith('...')) {
490
+ expr = trimmed.slice(0, -3).trim();
491
+ if (expr) {
492
+ attrs.push(new SpreadAttribute(new Expression(expr)));
493
+ }
494
+ continue;
452
495
  }
453
- result.push(source.slice(start));
454
- return result;
455
- }
456
- /** Find the first colon that is not inside brackets or strings. */
457
- function findColon(source) {
458
- let depth = 0;
459
- for (let i = 0; i < source.length; i++) {
460
- const ch = source[i];
461
- if (ch === '{' || ch === '[' || ch === '(')
462
- depth++;
463
- else if (ch === '}' || ch === ']' || ch === ')')
464
- depth--;
465
- else if ((ch === '"' || ch === "'") && depth === 0) {
466
- const q = ch;
467
- i++;
468
- while (i < source.length && source[i] !== q) {
469
- if (source[i] === '\\')
470
- i++;
471
- i++;
472
- }
496
+ if (trimmed.startsWith('...')) {
497
+ expr = trimmed.slice(3).trim();
498
+ if (expr) {
499
+ attrs.push(new SpreadAttribute(new Expression(expr)));
500
+ }
501
+ continue;
502
+ }
503
+ colonIdx = findColon(trimmed);
504
+ if (colonIdx === -1) {
505
+ // Shorthand: {foo} → foo={foo}
506
+ attrs.push(new Attribute(trimmed, new Expression(trimmed), true));
507
+ } else {
508
+ name = trimmed.slice(0, colonIdx).trim();
509
+ value = trimmed.slice(colonIdx + 1).trim();
510
+ cleanName = name.replace(/^['"]|['"]$/g, '');
511
+ attrs.push(new Attribute(cleanName, new Expression(value), false));
512
+ }
513
+ }
514
+ return attrs;
515
+ };
516
+
517
+ splitAttributePairs = function(source) {
518
+ var ch, depth, i, q, result, start;
519
+ result = [];
520
+ depth = 0;
521
+ start = 0;
522
+ i = 0;
523
+ while (i < source.length) {
524
+ ch = source[i];
525
+ if (ch === '{' || ch === '[' || ch === '(') {
526
+ depth++;
527
+ } else if (ch === '}' || ch === ']' || ch === ')') {
528
+ depth--;
529
+ } else if ((ch === '"' || ch === "'") && depth === 0) {
530
+ q = ch;
531
+ i++;
532
+ while (i < source.length && source[i] !== q) {
533
+ if (source[i] === '\\') {
534
+ i++;
473
535
  }
474
- else if (ch === ':' && depth === 0) {
475
- return i;
536
+ i++;
537
+ }
538
+ } else if (ch === ',' && depth === 0) {
539
+ result.push(source.slice(start, i));
540
+ start = i + 1;
541
+ }
542
+ i++;
543
+ }
544
+ result.push(source.slice(start));
545
+ return result;
546
+ };
547
+
548
+ findColon = function(source) {
549
+ var ch, depth, i, q;
550
+ depth = 0;
551
+ i = 0;
552
+ while (i < source.length) {
553
+ ch = source[i];
554
+ if (ch === '{' || ch === '[' || ch === '(') {
555
+ depth++;
556
+ } else if (ch === '}' || ch === ']' || ch === ')') {
557
+ depth--;
558
+ } else if ((ch === '"' || ch === "'") && depth === 0) {
559
+ q = ch;
560
+ i++;
561
+ while (i < source.length && source[i] !== q) {
562
+ if (source[i] === '\\') {
563
+ i++;
476
564
  }
565
+ i++;
566
+ }
567
+ } else if (ch === ':' && depth === 0) {
568
+ return i;
477
569
  }
478
- return -1;
479
- }
480
- //# sourceMappingURL=parser.js.map
570
+ i++;
571
+ }
572
+ return -1;
573
+ };
574
+
575
+ // ParseResult shape: { document: Document, errors: CompileError[] }