coffeehaml 0.6.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 (66) hide show
  1. package/README.md +21 -5
  2. package/dist/ast.js +334 -123
  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 -4
  9. package/dist/lexer.js +536 -340
  10. package/dist/parser.js +533 -432
  11. package/dist/prettier/coffeescript-formatter.js +309 -0
  12. package/dist/prettier/index.js +105 -0
  13. package/dist/prettier/options.js +232 -0
  14. package/dist/prettier/printer.js +559 -0
  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/docs/examples.md +201 -1
  29. package/docs/grammar.md +90 -1
  30. package/docs/index.html +260 -37
  31. package/package.json +12 -10
  32. package/syntaxes/CoffeeHaml.tmLanguage +815 -868
  33. package/syntaxes/coffeehaml.tmLanguage.json +352 -161
  34. package/dist/ast.d.ts +0 -117
  35. package/dist/ast.d.ts.map +0 -1
  36. package/dist/ast.js.map +0 -1
  37. package/dist/cli.d.ts +0 -3
  38. package/dist/cli.d.ts.map +0 -1
  39. package/dist/cli.js.map +0 -1
  40. package/dist/compiler.d.ts +0 -10
  41. package/dist/compiler.d.ts.map +0 -1
  42. package/dist/compiler.js.map +0 -1
  43. package/dist/emitter.d.ts +0 -4
  44. package/dist/emitter.d.ts.map +0 -1
  45. package/dist/emitter.js.map +0 -1
  46. package/dist/errors.d.ts +0 -3
  47. package/dist/errors.d.ts.map +0 -1
  48. package/dist/errors.js.map +0 -1
  49. package/dist/expressions.d.ts +0 -22
  50. package/dist/expressions.d.ts.map +0 -1
  51. package/dist/expressions.js.map +0 -1
  52. package/dist/index.d.ts +0 -5
  53. package/dist/index.d.ts.map +0 -1
  54. package/dist/index.js.map +0 -1
  55. package/dist/lexer.d.ts +0 -30
  56. package/dist/lexer.d.ts.map +0 -1
  57. package/dist/lexer.js.map +0 -1
  58. package/dist/parser.d.ts +0 -10
  59. package/dist/parser.d.ts.map +0 -1
  60. package/dist/parser.js.map +0 -1
  61. package/dist/types.d.ts +0 -76
  62. package/dist/types.d.ts.map +0 -1
  63. package/dist/types.js.map +0 -1
  64. package/dist/vite-plugin.d.ts +0 -7
  65. package/dist/vite-plugin.d.ts.map +0 -1
  66. package/dist/vite-plugin.js.map +0 -1
package/dist/lexer.js CHANGED
@@ -1,366 +1,562 @@
1
+ // Generated by CoffeeScript 2.7.0
2
+ // lexer.coffee — CoffeeHaml tokenizer
3
+ // Converts raw .chaml source into a token stream with INDENT/DEDENT
4
+ var countIndent, dedentToken, extractBracketed, indentToken, isHamlConstruct, parseModifiersAndAttrs, preMergeBracketLines, tokenizeLine;
5
+
6
+ import {
7
+ SourceLocation
8
+ } from './types.js';
9
+
1
10
  // ─── Token Types ───────────────────────────────────────────
2
- export var TokenType;
3
- (function (TokenType) {
4
- TokenType["TAG"] = "TAG";
5
- TokenType["CLASS"] = "CLASS";
6
- TokenType["ID"] = "ID";
7
- TokenType["ATTRS_PAREN"] = "ATTRS_PAREN";
8
- TokenType["ATTRS_BRACE"] = "ATTRS_BRACE";
9
- TokenType["OUTPUT"] = "OUTPUT";
10
- TokenType["OUTPUT_UNESC"] = "OUTPUT_UNESC";
11
- TokenType["CONTROL"] = "CONTROL";
12
- TokenType["COMMENT"] = "COMMENT";
13
- TokenType["HTML_COMMENT"] = "HTML_COMMENT";
14
- TokenType["FILTER"] = "FILTER";
15
- TokenType["DOCTYPE"] = "DOCTYPE";
16
- TokenType["TEXT"] = "TEXT";
17
- TokenType["PROLOGUE"] = "PROLOGUE";
18
- TokenType["INDENT"] = "INDENT";
19
- TokenType["DEDENT"] = "DEDENT";
20
- TokenType["NEWLINE"] = "NEWLINE";
21
- TokenType["SELF_CLOSE"] = "SELF_CLOSE";
22
- })(TokenType || (TokenType = {}));
23
- // ─── Lexer ─────────────────────────────────────────────────
24
- export function tokenize(source, filename) {
25
- const tokens = [];
26
- const lines = source.split('\n');
27
- const indentStack = [0]; // level 0 is always on the stack
28
- let offset = 0;
29
- let lineIndex = 0;
30
- let inPrologue = true; // true until first HAML construct encountered
31
- for (const rawLine of lines) {
32
- const line = rawLine.replace(/\r$/, '');
33
- const lineLength = rawLine.length + 1; // +1 for \n (used only for offset)
34
- // Skip empty lines (but preserve their space for offset tracking)
35
- if (line.trim() === '') {
36
- offset += lineLength;
37
- lineIndex++;
38
- continue;
11
+ export var TokenType = {
12
+ TAG: 'TAG',
13
+ CLASS: 'CLASS',
14
+ ID: 'ID',
15
+ ATTRS_PAREN: 'ATTRS_PAREN',
16
+ ATTRS_BRACE: 'ATTRS_BRACE',
17
+ OUTPUT: 'OUTPUT',
18
+ OUTPUT_UNESC: 'OUTPUT_UNESC',
19
+ CONTROL: 'CONTROL',
20
+ COMMENT: 'COMMENT',
21
+ HTML_COMMENT: 'HTML_COMMENT',
22
+ FILTER: 'FILTER',
23
+ DOCTYPE: 'DOCTYPE',
24
+ TEXT: 'TEXT',
25
+ PROLOGUE: 'PROLOGUE',
26
+ INDENT: 'INDENT',
27
+ DEDENT: 'DEDENT',
28
+ NEWLINE: 'NEWLINE',
29
+ SELF_CLOSE: 'SELF_CLOSE'
30
+ };
31
+
32
+ // ─── Token (plain object convention) ───────────────────────
33
+ // { type, value, location, indent? }
34
+
35
+ // ─── Main entry ────────────────────────────────────────────
36
+
37
+ // ─── Pre-processing: merge lines with unclosed attribute brackets ──
38
+ preMergeBracketLines = function(lines) {
39
+ var i, line, merged, rest, result;
40
+ result = [];
41
+ i = 0;
42
+ while (i < lines.length) {
43
+ line = lines[i];
44
+ rest = line.replace(/^\s+/, '');
45
+ // Check if this is a HAML tag/implicit-div line with unclosed {
46
+ if (/^[%.#]/.test(rest) && rest.includes('{') && !rest.includes('}')) {
47
+ merged = line;
48
+ i++;
49
+ while (i < lines.length) {
50
+ merged += '\n' + lines[i];
51
+ i++;
52
+ if (lines[i - 1].includes('}')) {
53
+ break;
39
54
  }
40
- const indent = countIndent(line);
41
- const content = line.slice(indent);
42
- // Skip blank lines
43
- if (content === '') {
44
- offset += lineLength;
45
- lineIndex++;
46
- continue;
55
+ }
56
+ result.push(merged);
57
+ } else if (/^[%.#]/.test(rest) && rest.includes('(') && !rest.includes(')')) {
58
+ merged = line;
59
+ i++;
60
+ while (i < lines.length) {
61
+ merged += '\n' + lines[i];
62
+ i++;
63
+ if (lines[i - 1].includes(')')) {
64
+ break;
47
65
  }
48
- // ── Prologue detection: non-indented JS lines before first HAML construct ──
49
- if (inPrologue && indent === 0 && !isHamlConstruct(content)) {
50
- tokens.push({
51
- type: TokenType.PROLOGUE,
52
- value: content,
53
- location: {
54
- start: { line: lineIndex, column: indent },
55
- end: { line: lineIndex, column: indent + content.length },
56
- offset: offset + indent,
57
- length: lineLength - indent,
58
- file: filename,
59
- },
60
- });
61
- offset += lineLength;
62
- lineIndex++;
63
- continue;
64
- }
65
- inPrologue = false; // first HAML construct or indented line locks us out of prologue
66
- // Handle indentation changes
67
- const currentIndent = indentStack[indentStack.length - 1];
68
- if (indent > currentIndent) {
69
- indentStack.push(indent);
70
- tokens.push(indentToken(indent, offset, indent, filename, lineIndex));
71
- }
72
- else if (indent < currentIndent) {
73
- while (indentStack.length > 1 && indent < indentStack[indentStack.length - 1]) {
74
- indentStack.pop();
75
- tokens.push(dedentToken(offset, filename, lineIndex));
76
- }
77
- if (indent !== indentStack[indentStack.length - 1]) {
78
- // Misaligned indent — still emit what we can
79
- indentStack.push(indent);
80
- tokens.push(indentToken(indent, offset, indent, filename, lineIndex));
81
- }
66
+ }
67
+ result.push(merged);
68
+ } else {
69
+ result.push(line);
70
+ i++;
71
+ }
72
+ }
73
+ return result;
74
+ };
75
+
76
+ export var tokenize = function(source, filename = null) {
77
+ var content, currentIndent, inPrologue, indent, indentStack, j, len, line, lineIndex, lineLength, lineStartOffset, lineTokens, lines, offset, rawLine, tokens;
78
+ tokens = [];
79
+ lines = preMergeBracketLines(source.split('\n'));
80
+ indentStack = [0];
81
+ offset = 0;
82
+ lineIndex = 0;
83
+ inPrologue = true;
84
+ for (j = 0, len = lines.length; j < len; j++) {
85
+ rawLine = lines[j];
86
+ line = rawLine.replace(/\r$/, '');
87
+ lineLength = rawLine.length + 1;
88
+ // Skip empty lines
89
+ if (line.trim() === '') {
90
+ offset += lineLength;
91
+ lineIndex++;
92
+ continue;
93
+ }
94
+ indent = countIndent(line);
95
+ content = line.slice(indent);
96
+ if (content === '') {
97
+ offset += lineLength;
98
+ lineIndex++;
99
+ continue;
100
+ }
101
+ // Prologue detection: non-indented JS before first HAML construct
102
+ if (inPrologue && indent === 0 && !isHamlConstruct(content)) {
103
+ tokens.push({
104
+ type: TokenType.PROLOGUE,
105
+ value: content,
106
+ location: {
107
+ start: {
108
+ line: lineIndex,
109
+ column: indent
110
+ },
111
+ end: {
112
+ line: lineIndex,
113
+ column: indent + content.length
114
+ },
115
+ offset: offset + indent,
116
+ length: lineLength - indent,
117
+ file: filename
82
118
  }
83
- // Parse the line content
84
- const lineStartOffset = offset + indent;
85
- const lineTokens = tokenizeLine(content, lineStartOffset, lineLength - indent, filename, lineIndex);
86
- tokens.push(...lineTokens);
87
- offset += lineLength;
88
- lineIndex++;
119
+ });
120
+ offset += lineLength;
121
+ lineIndex++;
122
+ continue;
89
123
  }
90
- // Emit remaining DEDENT tokens at EOF
91
- while (indentStack.length > 1) {
124
+ inPrologue = false;
125
+ // Handle indentation changes
126
+ currentIndent = indentStack[indentStack.length - 1];
127
+ if (indent > currentIndent) {
128
+ indentStack.push(indent);
129
+ tokens.push(indentToken(indent, offset, indent, filename, lineIndex));
130
+ } else if (indent < currentIndent) {
131
+ while (indentStack.length > 1 && indent < indentStack[indentStack.length - 1]) {
92
132
  indentStack.pop();
93
133
  tokens.push(dedentToken(offset, filename, lineIndex));
134
+ }
135
+ if (indent !== indentStack[indentStack.length - 1]) {
136
+ indentStack.push(indent);
137
+ tokens.push(indentToken(indent, offset, indent, filename, lineIndex));
138
+ }
94
139
  }
95
- return tokens;
96
- }
140
+ // Parse line content
141
+ lineStartOffset = offset + indent;
142
+ lineTokens = tokenizeLine(content, lineStartOffset, lineLength - indent, filename, lineIndex);
143
+ tokens.push(...lineTokens);
144
+ offset += lineLength;
145
+ lineIndex++;
146
+ }
147
+ // Emit remaining DEDENT tokens at EOF
148
+ while (indentStack.length > 1) {
149
+ indentStack.pop();
150
+ tokens.push(dedentToken(offset, filename, lineIndex));
151
+ }
152
+ return tokens;
153
+ };
154
+
97
155
  // ─── Line Tokenization ─────────────────────────────────────
98
- function tokenizeLine(content, lineStartOffset, _lineLength, filename, lineIndex) {
99
- const tokens = [];
100
- let pos = 0;
101
- function loc(start, end) {
102
- return {
103
- start: { line: lineIndex ?? 0, column: lineStartOffset + start },
104
- end: { line: lineIndex ?? 0, column: lineStartOffset + end },
105
- offset: lineStartOffset + start,
106
- length: end - start,
107
- file: filename,
108
- };
156
+ tokenizeLine = function(content, lineStartOffset, _lineLength, filename = null, lineIndex = 0) {
157
+ var end, expr, filterContent, filterName, firstChar, loc, modResult, pos, remaining, spaceIdx, tagName, tokens, trimmedOffset, val;
158
+ tokens = [];
159
+ pos = 0;
160
+ loc = function(start, end) {
161
+ return {
162
+ start: {
163
+ line: lineIndex != null ? lineIndex : 0,
164
+ column: lineStartOffset + start
165
+ },
166
+ end: {
167
+ line: lineIndex != null ? lineIndex : 0,
168
+ column: lineStartOffset + end
169
+ },
170
+ offset: lineStartOffset + start,
171
+ length: end - start,
172
+ file: filename
173
+ };
174
+ };
175
+ firstChar = content[0];
176
+ // %tag
177
+ if (firstChar === '%') {
178
+ end = 1;
179
+ while (end < content.length && /[\w-]/.test(content[end])) {
180
+ end++;
109
181
  }
110
- const firstChar = content[0];
111
- // ── %tag ──
112
- if (firstChar === '%') {
113
- let end = 1;
114
- // Read tag name: letters, digits, hyphens, underscores
115
- while (end < content.length && /[\w-]/.test(content[end])) {
116
- end++;
117
- }
118
- const tagName = content.slice(1, end);
119
- tokens.push({ type: TokenType.TAG, value: tagName, location: loc(0, end) });
120
- pos = end;
121
- // Check for self-closing /
122
- if (content[pos] === '/') {
123
- tokens.push({ type: TokenType.SELF_CLOSE, value: '/', location: loc(pos, pos + 1) });
124
- pos++;
125
- }
126
- // Parse modifiers and attributes on the rest of the line
127
- const modResult = parseModifiersAndAttrs(content, pos, lineStartOffset, filename, lineIndex);
128
- tokens.push(...modResult.tokens);
129
- pos = modResult.pos;
130
- // Parse inline output or text after tag and attributes
131
- const remaining = content.slice(pos).trimStart();
132
- const trimmedOffset = content.length - remaining.length;
133
- if (remaining) {
134
- if (remaining.startsWith('= ') || remaining === '=') {
135
- tokens.push({ type: TokenType.OUTPUT, value: remaining.slice(1).trimStart(), location: loc(trimmedOffset + 1, content.length) });
136
- }
137
- else if (remaining.startsWith('!= ')) {
138
- tokens.push({ type: TokenType.OUTPUT_UNESC, value: remaining.slice(2).trimStart(), location: loc(trimmedOffset + 2, content.length) });
139
- }
140
- else if (!remaining.startsWith('{') && !remaining.startsWith('(')) {
141
- tokens.push({
142
- type: TokenType.TEXT,
143
- value: remaining,
144
- location: loc(trimmedOffset, content.length),
145
- });
146
- }
147
- }
148
- return tokens;
182
+ tagName = content.slice(1, end);
183
+ tokens.push({
184
+ type: TokenType.TAG,
185
+ value: tagName,
186
+ location: loc(0, end)
187
+ });
188
+ pos = end;
189
+ // Self-closing /
190
+ if (content[pos] === '/') {
191
+ tokens.push({
192
+ type: TokenType.SELF_CLOSE,
193
+ value: '/',
194
+ location: loc(pos, pos + 1)
195
+ });
196
+ pos++;
149
197
  }
150
- // ── .class / #id (implicit div) ──
151
- // Only match if the modifier is immediately followed by a valid identifier char.
152
- // This prevents `# Hello` (Markdown heading) or `. something` from being
153
- // mistakenly parsed as modifiers inside filter blocks.
154
- if ((firstChar === '.' || firstChar === '#') && /^[.#][\w-]/.test(content)) {
155
- const modResult = parseModifiersAndAttrs(content, 0, lineStartOffset, filename, lineIndex);
156
- tokens.push(...modResult.tokens);
157
- let pos = modResult.pos;
158
- // Parse inline output or text after modifiers (same as %tag path)
159
- const remaining = content.slice(pos).trimStart();
160
- const trimmedOffset = content.length - remaining.length;
161
- if (remaining) {
162
- if (remaining.startsWith('= ') || remaining === '=') {
163
- tokens.push({ type: TokenType.OUTPUT, value: remaining.slice(1).trimStart(), location: loc(trimmedOffset + 1, content.length) });
164
- }
165
- else if (remaining.startsWith('!= ')) {
166
- tokens.push({ type: TokenType.OUTPUT_UNESC, value: remaining.slice(2).trimStart(), location: loc(trimmedOffset + 2, content.length) });
167
- }
168
- else if (!remaining.startsWith('{') && !remaining.startsWith('(')) {
169
- tokens.push({
170
- type: TokenType.TEXT,
171
- value: remaining,
172
- location: loc(trimmedOffset, content.length),
173
- });
174
- }
175
- }
176
- return tokens;
198
+ modResult = parseModifiersAndAttrs(content, pos, lineStartOffset, filename, lineIndex);
199
+ tokens.push(...modResult.tokens);
200
+ pos = modResult.pos;
201
+ // Self-closing / after attributes (must check BEFORE remaining text)
202
+ if (content[pos] === '/') {
203
+ tokens.push({
204
+ type: TokenType.SELF_CLOSE,
205
+ value: '/',
206
+ location: loc(pos, pos + 1)
207
+ });
208
+ pos++;
177
209
  }
178
- // ── = expression ──
179
- if (firstChar === '=' && content[1] !== '=') {
180
- const expr = content.slice(1).trimStart();
181
- return [{ type: TokenType.OUTPUT, value: expr, location: loc(1, content.length) }];
210
+ remaining = content.slice(pos).trimStart();
211
+ trimmedOffset = content.length - remaining.length;
212
+ if (remaining) {
213
+ if (remaining.startsWith('= ') || remaining === '=') {
214
+ tokens.push({
215
+ type: TokenType.OUTPUT,
216
+ value: remaining.slice(1).trimStart(),
217
+ location: loc(trimmedOffset + 1, content.length)
218
+ });
219
+ } else if (remaining.startsWith('!= ')) {
220
+ tokens.push({
221
+ type: TokenType.OUTPUT_UNESC,
222
+ value: remaining.slice(2).trimStart(),
223
+ location: loc(trimmedOffset + 2, content.length)
224
+ });
225
+ } else if (!(remaining.startsWith('{') || remaining.startsWith('('))) {
226
+ tokens.push({
227
+ type: TokenType.TEXT,
228
+ value: remaining,
229
+ location: loc(trimmedOffset, content.length)
230
+ });
231
+ }
182
232
  }
183
- // ── != expression ──
184
- if (firstChar === '!' && content[1] === '=') {
185
- const expr = content.slice(2).trimStart();
186
- return [{ type: TokenType.OUTPUT_UNESC, value: expr, location: loc(2, content.length) }];
233
+ return tokens;
234
+ }
235
+ // .class / #id (implicit div) — only if followed by valid identifier
236
+ if ((firstChar === '.' || firstChar === '#') && /^[.#][\w-]/.test(content)) {
237
+ modResult = parseModifiersAndAttrs(content, 0, lineStartOffset, filename, lineIndex);
238
+ tokens.push(...modResult.tokens);
239
+ pos = modResult.pos;
240
+ // Self-closing / after attributes (must check BEFORE remaining text)
241
+ if (content[pos] === '/') {
242
+ tokens.push({
243
+ type: TokenType.SELF_CLOSE,
244
+ value: '/',
245
+ location: loc(pos, pos + 1)
246
+ });
247
+ pos++;
248
+ }
249
+ remaining = content.slice(pos).trimStart();
250
+ trimmedOffset = content.length - remaining.length;
251
+ if (remaining) {
252
+ if (remaining.startsWith('= ') || remaining === '=') {
253
+ tokens.push({
254
+ type: TokenType.OUTPUT,
255
+ value: remaining.slice(1).trimStart(),
256
+ location: loc(trimmedOffset + 1, content.length)
257
+ });
258
+ } else if (remaining.startsWith('!= ')) {
259
+ tokens.push({
260
+ type: TokenType.OUTPUT_UNESC,
261
+ value: remaining.slice(2).trimStart(),
262
+ location: loc(trimmedOffset + 2, content.length)
263
+ });
264
+ } else if (!(remaining.startsWith('{') || remaining.startsWith('('))) {
265
+ tokens.push({
266
+ type: TokenType.TEXT,
267
+ value: remaining,
268
+ location: loc(trimmedOffset, content.length)
269
+ });
270
+ }
187
271
  }
188
- // ── - control / comment ──
189
- if (firstChar === '-') {
190
- if (content[1] === '#') {
191
- // Haml comment
192
- return [{ type: TokenType.COMMENT, value: content.slice(2).trimStart(), location: loc(2, content.length) }];
272
+ return tokens;
273
+ }
274
+ // = expression
275
+ if (firstChar === '=' && content[1] !== '=') {
276
+ expr = content.slice(1).trimStart();
277
+ return [
278
+ {
279
+ type: TokenType.OUTPUT,
280
+ value: expr,
281
+ location: loc(1,
282
+ content.length)
283
+ }
284
+ ];
285
+ }
286
+ // != expression
287
+ if (firstChar === '!' && content[1] === '=') {
288
+ expr = content.slice(2).trimStart();
289
+ return [
290
+ {
291
+ type: TokenType.OUTPUT_UNESC,
292
+ value: expr,
293
+ location: loc(2,
294
+ content.length)
295
+ }
296
+ ];
297
+ }
298
+ // - control / comment
299
+ if (firstChar === '-') {
300
+ if (content[1] === '#') {
301
+ return [
302
+ {
303
+ type: TokenType.COMMENT,
304
+ value: content.slice(2).trimStart(),
305
+ location: loc(2,
306
+ content.length)
193
307
  }
194
- // Control flow or code
195
- const expr = content.slice(1).trimStart();
196
- return [{ type: TokenType.CONTROL, value: expr, location: loc(1, content.length) }];
308
+ ];
309
+ }
310
+ expr = content.slice(1).trimStart();
311
+ return [
312
+ {
313
+ type: TokenType.CONTROL,
314
+ value: expr,
315
+ location: loc(1,
316
+ content.length)
317
+ }
318
+ ];
319
+ }
320
+ // / HTML comment
321
+ if (firstChar === '/') {
322
+ return [
323
+ {
324
+ type: TokenType.HTML_COMMENT,
325
+ value: content.slice(1).trimStart(),
326
+ location: loc(1,
327
+ content.length)
328
+ }
329
+ ];
330
+ }
331
+ // :filter
332
+ if (firstChar === ':') {
333
+ spaceIdx = content.search(/\s/);
334
+ filterName = spaceIdx === -1 ? content.slice(1) : content.slice(1, spaceIdx);
335
+ filterContent = spaceIdx === -1 ? '' : content.slice(spaceIdx + 1);
336
+ return [
337
+ {
338
+ type: TokenType.FILTER,
339
+ value: `${filterName}\n${filterContent}`,
340
+ location: loc(0,
341
+ content.length)
342
+ }
343
+ ];
344
+ }
345
+ // !!! doctype
346
+ if (content.startsWith('!!!')) {
347
+ val = content.slice(3).trimStart();
348
+ return [
349
+ {
350
+ type: TokenType.DOCTYPE,
351
+ value: val || 'html',
352
+ location: loc(0,
353
+ content.length)
354
+ }
355
+ ];
356
+ }
357
+ return [
358
+ {
359
+ // Plain text
360
+ type: TokenType.TEXT,
361
+ value: content,
362
+ location: loc(0,
363
+ content.length)
197
364
  }
198
- // ── / HTML comment ──
199
- if (firstChar === '/') {
200
- return [{ type: TokenType.HTML_COMMENT, value: content.slice(1).trimStart(), location: loc(1, content.length) }];
365
+ ];
366
+ };
367
+
368
+ // ─── Modifiers and Attributes ──────────────────────────────
369
+ parseModifiersAndAttrs = function(content, startPos, lineStartOffset, filename = null, lineIndex = 0) {
370
+ var block, ch, className, end, idName, loc, pos, tokens;
371
+ tokens = [];
372
+ pos = startPos;
373
+ loc = function(start, end) {
374
+ return {
375
+ start: {
376
+ line: lineIndex != null ? lineIndex : 0,
377
+ column: lineStartOffset + start
378
+ },
379
+ end: {
380
+ line: lineIndex != null ? lineIndex : 0,
381
+ column: lineStartOffset + end
382
+ },
383
+ offset: lineStartOffset + start,
384
+ length: end - start,
385
+ file: filename
386
+ };
387
+ };
388
+ while (pos < content.length) {
389
+ ch = content[pos];
390
+ if (ch === ' ' || ch === '\t') {
391
+ pos++;
392
+ continue;
201
393
  }
202
- // ── :filter ──
203
- if (firstChar === ':') {
204
- const spaceIdx = content.search(/\s/);
205
- const filterName = spaceIdx === -1 ? content.slice(1) : content.slice(1, spaceIdx);
206
- const filterContent = spaceIdx === -1 ? '' : content.slice(spaceIdx + 1);
207
- return [{ type: TokenType.FILTER, value: `${filterName}\n${filterContent}`, location: loc(0, content.length) }];
394
+ // .class
395
+ if (ch === '.') {
396
+ end = pos + 1;
397
+ while (end < content.length && /[\w-]/.test(content[end])) {
398
+ end++;
399
+ }
400
+ className = content.slice(pos + 1, end);
401
+ tokens.push({
402
+ type: TokenType.CLASS,
403
+ value: className,
404
+ location: loc(pos, end)
405
+ });
406
+ pos = end;
407
+ continue;
208
408
  }
209
- // ── !!! doctype ──
210
- if (content.startsWith('!!!')) {
211
- const val = content.slice(3).trimStart();
212
- return [{ type: TokenType.DOCTYPE, value: val || 'html', location: loc(0, content.length) }];
409
+ // #id
410
+ if (ch === '#') {
411
+ end = pos + 1;
412
+ while (end < content.length && /[\w-]/.test(content[end])) {
413
+ end++;
414
+ }
415
+ idName = content.slice(pos + 1, end);
416
+ tokens.push({
417
+ type: TokenType.ID,
418
+ value: idName,
419
+ location: loc(pos, end)
420
+ });
421
+ pos = end;
422
+ continue;
213
423
  }
214
- // ── Plain text ──
215
- return [{ type: TokenType.TEXT, value: content, location: loc(0, content.length) }];
216
- }
217
- // ─── Modifiers and Attributes Parser ───────────────────────
218
- function parseModifiersAndAttrs(content, startPos, lineStartOffset, filename, lineIndex) {
219
- const tokens = [];
220
- let pos = startPos;
221
- function loc(start, end) {
222
- return {
223
- start: { line: lineIndex ?? 0, column: lineStartOffset + start },
224
- end: { line: lineIndex ?? 0, column: lineStartOffset + end },
225
- offset: lineStartOffset + start,
226
- length: end - start,
227
- file: filename,
228
- };
424
+ // {attribute block}
425
+ if (ch === '{') {
426
+ block = extractBracketed(content, pos, '{', '}');
427
+ if (block !== null) {
428
+ tokens.push({
429
+ type: TokenType.ATTRS_BRACE,
430
+ value: block,
431
+ location: loc(pos, pos + block.length + 2)
432
+ });
433
+ pos += block.length + 2;
434
+ } else {
435
+ tokens.push({
436
+ type: TokenType.ATTRS_BRACE,
437
+ value: content.slice(pos + 1),
438
+ location: loc(pos, content.length)
439
+ });
440
+ pos = content.length;
441
+ }
442
+ continue;
229
443
  }
230
- while (pos < content.length) {
231
- const ch = content[pos];
232
- // Whitespace
233
- if (ch === ' ' || ch === '\t') {
234
- pos++;
235
- continue;
236
- }
237
- // .class
238
- if (ch === '.') {
239
- let end = pos + 1;
240
- while (end < content.length && /[\w-]/.test(content[end])) {
241
- end++;
242
- }
243
- const className = content.slice(pos + 1, end);
244
- tokens.push({ type: TokenType.CLASS, value: className, location: loc(pos, end) });
245
- pos = end;
246
- continue;
247
- }
248
- // #id
249
- if (ch === '#') {
250
- let end = pos + 1;
251
- while (end < content.length && /[\w-]/.test(content[end])) {
252
- end++;
253
- }
254
- const idName = content.slice(pos + 1, end);
255
- tokens.push({ type: TokenType.ID, value: idName, location: loc(pos, end) });
256
- pos = end;
257
- continue;
258
- }
259
- // {attribute block}
260
- if (ch === '{') {
261
- const block = extractBracketed(content, pos, '{', '}');
262
- if (block !== null) {
263
- tokens.push({ type: TokenType.ATTRS_BRACE, value: block, location: loc(pos, pos + block.length + 2) });
264
- pos += block.length + 2;
265
- }
266
- else {
267
- // Unterminated — consume rest
268
- tokens.push({ type: TokenType.ATTRS_BRACE, value: content.slice(pos + 1), location: loc(pos, content.length) });
269
- pos = content.length;
270
- }
271
- continue;
272
- }
273
- // (attribute block)
274
- if (ch === '(') {
275
- const block = extractBracketed(content, pos, '(', ')');
276
- if (block !== null) {
277
- tokens.push({ type: TokenType.ATTRS_PAREN, value: block, location: loc(pos, pos + block.length + 2) });
278
- pos += block.length + 2;
279
- }
280
- else {
281
- tokens.push({ type: TokenType.ATTRS_PAREN, value: content.slice(pos + 1), location: loc(pos, content.length) });
282
- pos = content.length;
283
- }
284
- continue;
285
- }
286
- // Anything else → stop, it's inline text
287
- break;
444
+ // (attribute block)
445
+ if (ch === '(') {
446
+ block = extractBracketed(content, pos, '(', ')');
447
+ if (block !== null) {
448
+ tokens.push({
449
+ type: TokenType.ATTRS_PAREN,
450
+ value: block,
451
+ location: loc(pos, pos + block.length + 2)
452
+ });
453
+ pos += block.length + 2;
454
+ } else {
455
+ tokens.push({
456
+ type: TokenType.ATTRS_PAREN,
457
+ value: content.slice(pos + 1),
458
+ location: loc(pos, content.length)
459
+ });
460
+ pos = content.length;
461
+ }
462
+ continue;
288
463
  }
289
- return { tokens, pos };
290
- }
464
+ // Anything else → stop
465
+ break;
466
+ }
467
+ return {tokens, pos};
468
+ };
469
+
291
470
  // ─── Helpers ───────────────────────────────────────────────
292
- /** Returns true if a line of content starts a HAML construct (not plain text). */
293
- function isHamlConstruct(content) {
294
- const fc = content[0];
295
- return fc === '%' || fc === '.' || fc === '#' || fc === '=' ||
296
- fc === '-' || fc === '/' || fc === ':' ||
297
- (fc === '!' && content[1] === '=') ||
298
- content.startsWith('!!!');
299
- }
300
- function countIndent(line) {
301
- let count = 0;
302
- for (const ch of line) {
303
- if (ch === ' ')
304
- count++;
305
- else if (ch === '\t')
306
- count += 2; // tab = 2 spaces
307
- else
308
- break;
471
+ isHamlConstruct = function(content) {
472
+ var fc;
473
+ fc = content[0];
474
+ return (fc === '%' || fc === '.' || fc === '#' || fc === '=' || fc === '-' || fc === '/' || fc === ':') || (fc === '!' && content[1] === '=') || content.startsWith('!!!');
475
+ };
476
+
477
+ countIndent = function(line) {
478
+ var ch, count, j, len;
479
+ count = 0;
480
+ for (j = 0, len = line.length; j < len; j++) {
481
+ ch = line[j];
482
+ if (ch === ' ') {
483
+ count++;
484
+ } else if (ch === '\t') {
485
+ count += 2;
486
+ } else {
487
+ break;
309
488
  }
310
- return count;
311
- }
312
- function indentToken(_indent, offset, level, filename, line) {
313
- return {
314
- type: TokenType.INDENT,
315
- value: '',
316
- indent: level,
317
- location: {
318
- start: { line: line ?? 0, column: 0 },
319
- end: { line: line ?? 0, column: 0 },
320
- offset,
321
- length: 0,
322
- file: filename,
323
- },
324
- };
325
- }
326
- function dedentToken(offset, filename, line) {
327
- return {
328
- type: TokenType.DEDENT,
329
- value: '',
330
- indent: undefined,
331
- location: {
332
- start: { line: line ?? 0, column: 0 },
333
- end: { line: line ?? 0, column: 0 },
334
- offset,
335
- length: 0,
336
- file: filename,
337
- },
338
- };
339
- }
340
- /** Extract content between balanced open/close brackets. Returns inner content or null. */
341
- function extractBracketed(content, start, open, close) {
342
- let depth = 0;
343
- let pos = start;
344
- while (pos < content.length) {
345
- if (content[pos] === open)
346
- depth++;
347
- else if (content[pos] === close) {
348
- depth--;
349
- if (depth === 0)
350
- return content.slice(start + 1, pos);
351
- }
352
- // Handle string literals — skip over them
353
- if (content[pos] === '"' || content[pos] === "'") {
354
- const q = content[pos];
355
- pos++;
356
- while (pos < content.length && content[pos] !== q) {
357
- if (content[pos] === '\\')
358
- pos++; // skip escape
359
- pos++;
360
- }
489
+ }
490
+ return count;
491
+ };
492
+
493
+ indentToken = function(_indent, offset, level, filename = null, line = 0) {
494
+ return {
495
+ type: TokenType.INDENT,
496
+ value: '',
497
+ indent: level,
498
+ location: {
499
+ start: {
500
+ line: line != null ? line : 0,
501
+ column: 0
502
+ },
503
+ end: {
504
+ line: line != null ? line : 0,
505
+ column: 0
506
+ },
507
+ offset,
508
+ length: 0,
509
+ file: filename
510
+ }
511
+ };
512
+ };
513
+
514
+ dedentToken = function(offset, filename = null, line = 0) {
515
+ return {
516
+ type: TokenType.DEDENT,
517
+ value: '',
518
+ indent: void 0,
519
+ location: {
520
+ start: {
521
+ line: line != null ? line : 0,
522
+ column: 0
523
+ },
524
+ end: {
525
+ line: line != null ? line : 0,
526
+ column: 0
527
+ },
528
+ offset,
529
+ length: 0,
530
+ file: filename
531
+ }
532
+ };
533
+ };
534
+
535
+ extractBracketed = function(content, start, open, close) {
536
+ var depth, pos, q, ref;
537
+ depth = 0;
538
+ pos = start;
539
+ while (pos < content.length) {
540
+ if (content[pos] === open) {
541
+ depth++;
542
+ } else if (content[pos] === close) {
543
+ depth--;
544
+ if (depth === 0) {
545
+ return content.slice(start + 1, pos);
546
+ }
547
+ }
548
+ // Skip string literals
549
+ if ((ref = content[pos]) === '"' || ref === "'") {
550
+ q = content[pos];
551
+ pos++;
552
+ while (pos < content.length && content[pos] !== q) {
553
+ if (content[pos] === '\\') {
554
+ pos++;
361
555
  }
362
556
  pos++;
557
+ }
363
558
  }
364
- return null; // unbalanced
365
- }
366
- //# sourceMappingURL=lexer.js.map
559
+ pos++;
560
+ }
561
+ return null;
562
+ };