coffeehaml 0.7.1 → 0.7.3

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/dist/src/lexer.js DELETED
@@ -1,504 +0,0 @@
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, tokenizeLine;
5
-
6
- import {
7
- SourceLocation
8
- } from './types.js';
9
-
10
- // ─── Token Types ───────────────────────────────────────────
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
- export var tokenize = function(source, filename = null) {
37
- var content, currentIndent, i, inPrologue, indent, indentStack, len, line, lineIndex, lineLength, lineStartOffset, lineTokens, lines, offset, rawLine, tokens;
38
- tokens = [];
39
- lines = source.split('\n');
40
- indentStack = [0];
41
- offset = 0;
42
- lineIndex = 0;
43
- inPrologue = true;
44
- for (i = 0, len = lines.length; i < len; i++) {
45
- rawLine = lines[i];
46
- line = rawLine.replace(/\r$/, '');
47
- lineLength = rawLine.length + 1;
48
- // Skip empty lines
49
- if (line.trim() === '') {
50
- offset += lineLength;
51
- lineIndex++;
52
- continue;
53
- }
54
- indent = countIndent(line);
55
- content = line.slice(indent);
56
- if (content === '') {
57
- offset += lineLength;
58
- lineIndex++;
59
- continue;
60
- }
61
- // Prologue detection: non-indented JS before first HAML construct
62
- if (inPrologue && indent === 0 && !isHamlConstruct(content)) {
63
- tokens.push({
64
- type: TokenType.PROLOGUE,
65
- value: content,
66
- location: {
67
- start: {
68
- line: lineIndex,
69
- column: indent
70
- },
71
- end: {
72
- line: lineIndex,
73
- column: indent + content.length
74
- },
75
- offset: offset + indent,
76
- length: lineLength - indent,
77
- file: filename
78
- }
79
- });
80
- offset += lineLength;
81
- lineIndex++;
82
- continue;
83
- }
84
- inPrologue = false;
85
- // Handle indentation changes
86
- currentIndent = indentStack[indentStack.length - 1];
87
- if (indent > currentIndent) {
88
- indentStack.push(indent);
89
- tokens.push(indentToken(indent, offset, indent, filename, lineIndex));
90
- } else if (indent < currentIndent) {
91
- while (indentStack.length > 1 && indent < indentStack[indentStack.length - 1]) {
92
- indentStack.pop();
93
- tokens.push(dedentToken(offset, filename, lineIndex));
94
- }
95
- if (indent !== indentStack[indentStack.length - 1]) {
96
- indentStack.push(indent);
97
- tokens.push(indentToken(indent, offset, indent, filename, lineIndex));
98
- }
99
- }
100
- // Parse line content
101
- lineStartOffset = offset + indent;
102
- lineTokens = tokenizeLine(content, lineStartOffset, lineLength - indent, filename, lineIndex);
103
- tokens.push(...lineTokens);
104
- offset += lineLength;
105
- lineIndex++;
106
- }
107
- // Emit remaining DEDENT tokens at EOF
108
- while (indentStack.length > 1) {
109
- indentStack.pop();
110
- tokens.push(dedentToken(offset, filename, lineIndex));
111
- }
112
- return tokens;
113
- };
114
-
115
- // ─── Line Tokenization ─────────────────────────────────────
116
- tokenizeLine = function(content, lineStartOffset, _lineLength, filename = null, lineIndex = 0) {
117
- var end, expr, filterContent, filterName, firstChar, loc, modResult, pos, remaining, spaceIdx, tagName, tokens, trimmedOffset, val;
118
- tokens = [];
119
- pos = 0;
120
- loc = function(start, end) {
121
- return {
122
- start: {
123
- line: lineIndex != null ? lineIndex : 0,
124
- column: lineStartOffset + start
125
- },
126
- end: {
127
- line: lineIndex != null ? lineIndex : 0,
128
- column: lineStartOffset + end
129
- },
130
- offset: lineStartOffset + start,
131
- length: end - start,
132
- file: filename
133
- };
134
- };
135
- firstChar = content[0];
136
- // %tag
137
- if (firstChar === '%') {
138
- end = 1;
139
- while (end < content.length && /[\w-]/.test(content[end])) {
140
- end++;
141
- }
142
- tagName = content.slice(1, end);
143
- tokens.push({
144
- type: TokenType.TAG,
145
- value: tagName,
146
- location: loc(0, end)
147
- });
148
- pos = end;
149
- // Self-closing /
150
- if (content[pos] === '/') {
151
- tokens.push({
152
- type: TokenType.SELF_CLOSE,
153
- value: '/',
154
- location: loc(pos, pos + 1)
155
- });
156
- pos++;
157
- }
158
- modResult = parseModifiersAndAttrs(content, pos, lineStartOffset, filename, lineIndex);
159
- tokens.push(...modResult.tokens);
160
- pos = modResult.pos;
161
- remaining = content.slice(pos).trimStart();
162
- trimmedOffset = content.length - remaining.length;
163
- if (remaining) {
164
- if (remaining.startsWith('= ') || remaining === '=') {
165
- tokens.push({
166
- type: TokenType.OUTPUT,
167
- value: remaining.slice(1).trimStart(),
168
- location: loc(trimmedOffset + 1, content.length)
169
- });
170
- } else if (remaining.startsWith('!= ')) {
171
- tokens.push({
172
- type: TokenType.OUTPUT_UNESC,
173
- value: remaining.slice(2).trimStart(),
174
- location: loc(trimmedOffset + 2, content.length)
175
- });
176
- } else if (!(remaining.startsWith('{') || remaining.startsWith('('))) {
177
- tokens.push({
178
- type: TokenType.TEXT,
179
- value: remaining,
180
- location: loc(trimmedOffset, content.length)
181
- });
182
- }
183
- }
184
- return tokens;
185
- }
186
- // .class / #id (implicit div) — only if followed by valid identifier
187
- if ((firstChar === '.' || firstChar === '#') && /^[.#][\w-]/.test(content)) {
188
- modResult = parseModifiersAndAttrs(content, 0, lineStartOffset, filename, lineIndex);
189
- tokens.push(...modResult.tokens);
190
- pos = modResult.pos;
191
- remaining = content.slice(pos).trimStart();
192
- trimmedOffset = content.length - remaining.length;
193
- if (remaining) {
194
- if (remaining.startsWith('= ') || remaining === '=') {
195
- tokens.push({
196
- type: TokenType.OUTPUT,
197
- value: remaining.slice(1).trimStart(),
198
- location: loc(trimmedOffset + 1, content.length)
199
- });
200
- } else if (remaining.startsWith('!= ')) {
201
- tokens.push({
202
- type: TokenType.OUTPUT_UNESC,
203
- value: remaining.slice(2).trimStart(),
204
- location: loc(trimmedOffset + 2, content.length)
205
- });
206
- } else if (!(remaining.startsWith('{') || remaining.startsWith('('))) {
207
- tokens.push({
208
- type: TokenType.TEXT,
209
- value: remaining,
210
- location: loc(trimmedOffset, content.length)
211
- });
212
- }
213
- }
214
- return tokens;
215
- }
216
- // = expression
217
- if (firstChar === '=' && content[1] !== '=') {
218
- expr = content.slice(1).trimStart();
219
- return [
220
- {
221
- type: TokenType.OUTPUT,
222
- value: expr,
223
- location: loc(1,
224
- content.length)
225
- }
226
- ];
227
- }
228
- // != expression
229
- if (firstChar === '!' && content[1] === '=') {
230
- expr = content.slice(2).trimStart();
231
- return [
232
- {
233
- type: TokenType.OUTPUT_UNESC,
234
- value: expr,
235
- location: loc(2,
236
- content.length)
237
- }
238
- ];
239
- }
240
- // - control / comment
241
- if (firstChar === '-') {
242
- if (content[1] === '#') {
243
- return [
244
- {
245
- type: TokenType.COMMENT,
246
- value: content.slice(2).trimStart(),
247
- location: loc(2,
248
- content.length)
249
- }
250
- ];
251
- }
252
- expr = content.slice(1).trimStart();
253
- return [
254
- {
255
- type: TokenType.CONTROL,
256
- value: expr,
257
- location: loc(1,
258
- content.length)
259
- }
260
- ];
261
- }
262
- // / HTML comment
263
- if (firstChar === '/') {
264
- return [
265
- {
266
- type: TokenType.HTML_COMMENT,
267
- value: content.slice(1).trimStart(),
268
- location: loc(1,
269
- content.length)
270
- }
271
- ];
272
- }
273
- // :filter
274
- if (firstChar === ':') {
275
- spaceIdx = content.search(/\s/);
276
- filterName = spaceIdx === -1 ? content.slice(1) : content.slice(1, spaceIdx);
277
- filterContent = spaceIdx === -1 ? '' : content.slice(spaceIdx + 1);
278
- return [
279
- {
280
- type: TokenType.FILTER,
281
- value: `${filterName}\n${filterContent}`,
282
- location: loc(0,
283
- content.length)
284
- }
285
- ];
286
- }
287
- // !!! doctype
288
- if (content.startsWith('!!!')) {
289
- val = content.slice(3).trimStart();
290
- return [
291
- {
292
- type: TokenType.DOCTYPE,
293
- value: val || 'html',
294
- location: loc(0,
295
- content.length)
296
- }
297
- ];
298
- }
299
- return [
300
- {
301
- // Plain text
302
- type: TokenType.TEXT,
303
- value: content,
304
- location: loc(0,
305
- content.length)
306
- }
307
- ];
308
- };
309
-
310
- // ─── Modifiers and Attributes ──────────────────────────────
311
- parseModifiersAndAttrs = function(content, startPos, lineStartOffset, filename = null, lineIndex = 0) {
312
- var block, ch, className, end, idName, loc, pos, tokens;
313
- tokens = [];
314
- pos = startPos;
315
- loc = function(start, end) {
316
- return {
317
- start: {
318
- line: lineIndex != null ? lineIndex : 0,
319
- column: lineStartOffset + start
320
- },
321
- end: {
322
- line: lineIndex != null ? lineIndex : 0,
323
- column: lineStartOffset + end
324
- },
325
- offset: lineStartOffset + start,
326
- length: end - start,
327
- file: filename
328
- };
329
- };
330
- while (pos < content.length) {
331
- ch = content[pos];
332
- if (ch === ' ' || ch === '\t') {
333
- pos++;
334
- continue;
335
- }
336
- // .class
337
- if (ch === '.') {
338
- end = pos + 1;
339
- while (end < content.length && /[\w-]/.test(content[end])) {
340
- end++;
341
- }
342
- className = content.slice(pos + 1, end);
343
- tokens.push({
344
- type: TokenType.CLASS,
345
- value: className,
346
- location: loc(pos, end)
347
- });
348
- pos = end;
349
- continue;
350
- }
351
- // #id
352
- if (ch === '#') {
353
- end = pos + 1;
354
- while (end < content.length && /[\w-]/.test(content[end])) {
355
- end++;
356
- }
357
- idName = content.slice(pos + 1, end);
358
- tokens.push({
359
- type: TokenType.ID,
360
- value: idName,
361
- location: loc(pos, end)
362
- });
363
- pos = end;
364
- continue;
365
- }
366
- // {attribute block}
367
- if (ch === '{') {
368
- block = extractBracketed(content, pos, '{', '}');
369
- if (block !== null) {
370
- tokens.push({
371
- type: TokenType.ATTRS_BRACE,
372
- value: block,
373
- location: loc(pos, pos + block.length + 2)
374
- });
375
- pos += block.length + 2;
376
- } else {
377
- tokens.push({
378
- type: TokenType.ATTRS_BRACE,
379
- value: content.slice(pos + 1),
380
- location: loc(pos, content.length)
381
- });
382
- pos = content.length;
383
- }
384
- continue;
385
- }
386
- // (attribute block)
387
- if (ch === '(') {
388
- block = extractBracketed(content, pos, '(', ')');
389
- if (block !== null) {
390
- tokens.push({
391
- type: TokenType.ATTRS_PAREN,
392
- value: block,
393
- location: loc(pos, pos + block.length + 2)
394
- });
395
- pos += block.length + 2;
396
- } else {
397
- tokens.push({
398
- type: TokenType.ATTRS_PAREN,
399
- value: content.slice(pos + 1),
400
- location: loc(pos, content.length)
401
- });
402
- pos = content.length;
403
- }
404
- continue;
405
- }
406
- // Anything else → stop
407
- break;
408
- }
409
- return {tokens, pos};
410
- };
411
-
412
- // ─── Helpers ───────────────────────────────────────────────
413
- isHamlConstruct = function(content) {
414
- var fc;
415
- fc = content[0];
416
- return (fc === '%' || fc === '.' || fc === '#' || fc === '=' || fc === '-' || fc === '/' || fc === ':') || (fc === '!' && content[1] === '=') || content.startsWith('!!!');
417
- };
418
-
419
- countIndent = function(line) {
420
- var ch, count, i, len;
421
- count = 0;
422
- for (i = 0, len = line.length; i < len; i++) {
423
- ch = line[i];
424
- if (ch === ' ') {
425
- count++;
426
- } else if (ch === '\t') {
427
- count += 2;
428
- } else {
429
- break;
430
- }
431
- }
432
- return count;
433
- };
434
-
435
- indentToken = function(_indent, offset, level, filename = null, line = 0) {
436
- return {
437
- type: TokenType.INDENT,
438
- value: '',
439
- indent: level,
440
- location: {
441
- start: {
442
- line: line != null ? line : 0,
443
- column: 0
444
- },
445
- end: {
446
- line: line != null ? line : 0,
447
- column: 0
448
- },
449
- offset,
450
- length: 0,
451
- file: filename
452
- }
453
- };
454
- };
455
-
456
- dedentToken = function(offset, filename = null, line = 0) {
457
- return {
458
- type: TokenType.DEDENT,
459
- value: '',
460
- indent: void 0,
461
- location: {
462
- start: {
463
- line: line != null ? line : 0,
464
- column: 0
465
- },
466
- end: {
467
- line: line != null ? line : 0,
468
- column: 0
469
- },
470
- offset,
471
- length: 0,
472
- file: filename
473
- }
474
- };
475
- };
476
-
477
- extractBracketed = function(content, start, open, close) {
478
- var depth, pos, q, ref;
479
- depth = 0;
480
- pos = start;
481
- while (pos < content.length) {
482
- if (content[pos] === open) {
483
- depth++;
484
- } else if (content[pos] === close) {
485
- depth--;
486
- if (depth === 0) {
487
- return content.slice(start + 1, pos);
488
- }
489
- }
490
- // Skip string literals
491
- if ((ref = content[pos]) === '"' || ref === "'") {
492
- q = content[pos];
493
- pos++;
494
- while (pos < content.length && content[pos] !== q) {
495
- if (content[pos] === '\\') {
496
- pos++;
497
- }
498
- pos++;
499
- }
500
- }
501
- pos++;
502
- }
503
- return null;
504
- };