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.
- package/README.md +21 -5
- package/dist/ast.js +334 -125
- package/dist/cli.js +363 -291
- package/dist/compiler.js +126 -89
- package/dist/emitter.js +771 -736
- package/dist/errors.js +5 -2
- package/dist/expressions.js +211 -154
- package/dist/index.js +22 -6
- package/dist/lexer.js +536 -340
- package/dist/parser.js +533 -438
- package/dist/prettier/coffeescript-formatter.js +292 -392
- package/dist/prettier/index.js +96 -60
- package/dist/prettier/options.js +179 -147
- package/dist/prettier/printer.js +549 -642
- package/dist/types.js +122 -12
- package/dist/vite-plugin.js +78 -63
- package/package.json +4 -9
- package/dist/ast.d.ts +0 -123
- package/dist/ast.d.ts.map +0 -1
- package/dist/ast.js.map +0 -1
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/compiler.d.ts +0 -10
- package/dist/compiler.d.ts.map +0 -1
- package/dist/compiler.js.map +0 -1
- package/dist/emitter.d.ts +0 -4
- package/dist/emitter.d.ts.map +0 -1
- package/dist/emitter.js.map +0 -1
- package/dist/errors.d.ts +0 -3
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/expressions.d.ts +0 -22
- package/dist/expressions.d.ts.map +0 -1
- package/dist/expressions.js.map +0 -1
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lexer.d.ts +0 -30
- package/dist/lexer.d.ts.map +0 -1
- package/dist/lexer.js.map +0 -1
- package/dist/parser.d.ts +0 -10
- package/dist/parser.d.ts.map +0 -1
- package/dist/parser.js.map +0 -1
- package/dist/prettier/coffeescript-formatter.d.ts +0 -31
- package/dist/prettier/coffeescript-formatter.d.ts.map +0 -1
- package/dist/prettier/coffeescript-formatter.js.map +0 -1
- package/dist/prettier/index.d.ts +0 -8
- package/dist/prettier/index.d.ts.map +0 -1
- package/dist/prettier/index.js.map +0 -1
- package/dist/prettier/options.d.ts +0 -10
- package/dist/prettier/options.d.ts.map +0 -1
- package/dist/prettier/options.js.map +0 -1
- package/dist/prettier/printer.d.ts +0 -45
- package/dist/prettier/printer.d.ts.map +0 -1
- package/dist/prettier/printer.js.map +0 -1
- package/dist/types.d.ts +0 -76
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/vite-plugin.d.ts +0 -7
- package/dist/vite-plugin.d.ts.map +0 -1
- package/dist/vite-plugin.js.map +0 -1
package/dist/parser.js
CHANGED
|
@@ -1,480 +1,575 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
56
|
+
var token;
|
|
57
|
+
token = this.current();
|
|
58
|
+
this.pos++;
|
|
59
|
+
return token;
|
|
22
60
|
}
|
|
23
|
-
|
|
61
|
+
|
|
24
62
|
error(message, code = 'PARSE_ERROR') {
|
|
25
|
-
|
|
63
|
+
var ref;
|
|
64
|
+
return this.errors.push(new CompileError(message, 'parser', code, (ref = this.current()) != null ? ref.location : void 0));
|
|
26
65
|
}
|
|
27
|
-
|
|
66
|
+
|
|
28
67
|
expect(type) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
77
|
+
|
|
39
78
|
recover() {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
83
|
-
|
|
141
|
+
}
|
|
142
|
+
return nodes;
|
|
143
|
+
};
|
|
144
|
+
|
|
84
145
|
// ─── Node Dispatcher ───────────────────────────────────────
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
229
|
-
|
|
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
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
271
|
-
state.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
301
|
-
|
|
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
|
-
|
|
357
|
-
|
|
450
|
+
}
|
|
451
|
+
return new Filter(filterName, content, token.location);
|
|
452
|
+
};
|
|
453
|
+
|
|
358
454
|
// ─── Doctype ───────────────────────────────────────────────
|
|
359
|
-
function
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
return new Doctype(
|
|
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
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
return new Text(
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
|
|
475
|
-
|
|
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
|
-
|
|
479
|
-
}
|
|
480
|
-
|
|
570
|
+
i++;
|
|
571
|
+
}
|
|
572
|
+
return -1;
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
// ParseResult shape: { document: Document, errors: CompileError[] }
|