puglite 1.0.0

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/lib/lexer.js ADDED
@@ -0,0 +1,1713 @@
1
+ 'use strict';
2
+
3
+ var assert = require('assert');
4
+ var isExpression = require('is-expression');
5
+ var characterParser = require('character-parser');
6
+ var error = require('./error');
7
+
8
+ module.exports = lex;
9
+ module.exports.Lexer = Lexer;
10
+ function lex(str, options) {
11
+ var lexer = new Lexer(str, options);
12
+ return JSON.parse(JSON.stringify(lexer.getTokens()));
13
+ }
14
+
15
+ /**
16
+ * Initialize `Lexer` with the given `str`.
17
+ *
18
+ * @param {String} str
19
+ * @param {String} filename
20
+ * @api private
21
+ */
22
+
23
+ function Lexer(str, options) {
24
+ options = options || {};
25
+ if (typeof str !== 'string') {
26
+ throw new Error(
27
+ 'Expected source code to be a string but got "' + typeof str + '"'
28
+ );
29
+ }
30
+ if (typeof options !== 'object') {
31
+ throw new Error(
32
+ 'Expected "options" to be an object but got "' + typeof options + '"'
33
+ );
34
+ }
35
+ //Strip any UTF-8 BOM off of the start of `str`, if it exists.
36
+ str = str.replace(/^\uFEFF/, '');
37
+ this.input = str.replace(/\r\n|\r/g, '\n');
38
+ this.originalInput = this.input;
39
+ this.filename = options.filename;
40
+ this.interpolated = options.interpolated || false;
41
+ this.lineno = options.startingLine || 1;
42
+ this.colno = options.startingColumn || 1;
43
+ this.plugins = options.plugins || [];
44
+ this.indentStack = [0];
45
+ this.indentRe = null;
46
+ // If #{}, !{} or #[] syntax is allowed when adding text
47
+ this.interpolationAllowed = true;
48
+ this.whitespaceRe = /[ \n\t]/;
49
+
50
+ this.tokens = [];
51
+ this.ended = false;
52
+ }
53
+
54
+ /**
55
+ * Lexer prototype.
56
+ */
57
+
58
+ Lexer.prototype = {
59
+ constructor: Lexer,
60
+
61
+ error: function(code, message) {
62
+ var err = error(code, message, {
63
+ line: this.lineno,
64
+ column: this.colno,
65
+ filename: this.filename,
66
+ src: this.originalInput,
67
+ });
68
+ throw err;
69
+ },
70
+
71
+ assert: function(value, message) {
72
+ if (!value) this.error('ASSERT_FAILED', message);
73
+ },
74
+
75
+ isExpression: function(exp) {
76
+ return isExpression(exp, {
77
+ throw: true,
78
+ });
79
+ },
80
+
81
+ assertExpression: function(exp, noThrow) {
82
+ //this verifies that a JavaScript expression is valid
83
+ try {
84
+ this.callLexerFunction('isExpression', exp);
85
+ return true;
86
+ } catch (ex) {
87
+ if (noThrow) return false;
88
+
89
+ // not coming from acorn
90
+ if (!ex.loc) throw ex;
91
+
92
+ this.incrementLine(ex.loc.line - 1);
93
+ this.incrementColumn(ex.loc.column);
94
+ var msg =
95
+ 'Syntax Error: ' + ex.message.replace(/ \([0-9]+:[0-9]+\)$/, '');
96
+ this.error('SYNTAX_ERROR', msg);
97
+ }
98
+ },
99
+
100
+ assertNestingCorrect: function(exp) {
101
+ //this verifies that code is properly nested, but allows
102
+ //invalid JavaScript such as the contents of `attributes`
103
+ var res = characterParser.default(exp);
104
+ if (res.isNesting()) {
105
+ this.error(
106
+ 'INCORRECT_NESTING',
107
+ 'Nesting must match on expression `' + exp + '`'
108
+ );
109
+ }
110
+ },
111
+
112
+ /**
113
+ * Construct a token with the given `type` and `val`.
114
+ *
115
+ * @param {String} type
116
+ * @param {String} val
117
+ * @return {Object}
118
+ * @api private
119
+ */
120
+
121
+ tok: function(type, val) {
122
+ var res = {
123
+ type: type,
124
+ loc: {
125
+ start: {
126
+ line: this.lineno,
127
+ column: this.colno,
128
+ },
129
+ filename: this.filename,
130
+ },
131
+ };
132
+
133
+ if (val !== undefined) res.val = val;
134
+
135
+ return res;
136
+ },
137
+
138
+ /**
139
+ * Set the token's `loc.end` value.
140
+ *
141
+ * @param {Object} tok
142
+ * @returns {Object}
143
+ * @api private
144
+ */
145
+
146
+ tokEnd: function(tok) {
147
+ tok.loc.end = {
148
+ line: this.lineno,
149
+ column: this.colno,
150
+ };
151
+ return tok;
152
+ },
153
+
154
+ /**
155
+ * Increment `this.lineno` and reset `this.colno`.
156
+ *
157
+ * @param {Number} increment
158
+ * @api private
159
+ */
160
+
161
+ incrementLine: function(increment) {
162
+ this.lineno += increment;
163
+ if (increment) this.colno = 1;
164
+ },
165
+
166
+ /**
167
+ * Increment `this.colno`.
168
+ *
169
+ * @param {Number} increment
170
+ * @api private
171
+ */
172
+
173
+ incrementColumn: function(increment) {
174
+ this.colno += increment;
175
+ },
176
+
177
+ /**
178
+ * Consume the given `len` of input.
179
+ *
180
+ * @param {Number} len
181
+ * @api private
182
+ */
183
+
184
+ consume: function(len) {
185
+ this.input = this.input.substr(len);
186
+ },
187
+
188
+ /**
189
+ * Scan for `type` with the given `regexp`.
190
+ *
191
+ * @param {String} type
192
+ * @param {RegExp} regexp
193
+ * @return {Object}
194
+ * @api private
195
+ */
196
+
197
+ scan: function(regexp, type) {
198
+ var captures;
199
+ if ((captures = regexp.exec(this.input))) {
200
+ var len = captures[0].length;
201
+ var val = captures[1];
202
+ var diff = len - (val ? val.length : 0);
203
+ var tok = this.tok(type, val);
204
+ this.consume(len);
205
+ this.incrementColumn(diff);
206
+ return tok;
207
+ }
208
+ },
209
+ scanEndOfLine: function(regexp, type) {
210
+ var captures;
211
+ if ((captures = regexp.exec(this.input))) {
212
+ var whitespaceLength = 0;
213
+ var whitespace;
214
+ var tok;
215
+ if ((whitespace = /^([ ]+)([^ ]*)/.exec(captures[0]))) {
216
+ whitespaceLength = whitespace[1].length;
217
+ this.incrementColumn(whitespaceLength);
218
+ }
219
+ var newInput = this.input.substr(captures[0].length);
220
+ if (newInput[0] === ':') {
221
+ this.input = newInput;
222
+ tok = this.tok(type, captures[1]);
223
+ this.incrementColumn(captures[0].length - whitespaceLength);
224
+ return tok;
225
+ }
226
+ if (/^[ \t]*(\n|$)/.test(newInput)) {
227
+ this.input = newInput.substr(/^[ \t]*/.exec(newInput)[0].length);
228
+ tok = this.tok(type, captures[1]);
229
+ this.incrementColumn(captures[0].length - whitespaceLength);
230
+ return tok;
231
+ }
232
+ }
233
+ },
234
+
235
+ /**
236
+ * Return the indexOf `(` or `{` or `[` / `)` or `}` or `]` delimiters.
237
+ *
238
+ * Make sure that when calling this function, colno is at the character
239
+ * immediately before the beginning.
240
+ *
241
+ * @return {Number}
242
+ * @api private
243
+ */
244
+
245
+ bracketExpression: function(skip) {
246
+ skip = skip || 0;
247
+ var start = this.input[skip];
248
+ assert(
249
+ start === '(' || start === '{' || start === '[',
250
+ 'The start character should be "(", "{" or "["'
251
+ );
252
+ var end = {'(': ')', '{': '}', '[': ']'}[start];
253
+ var range;
254
+ try {
255
+ range = characterParser.parseUntil(this.input, end, {start: skip + 1});
256
+ } catch (ex) {
257
+ if (ex.index !== undefined) {
258
+ var idx = ex.index;
259
+ // starting from this.input[skip]
260
+ var tmp = this.input.substr(skip).indexOf('\n');
261
+ // starting from this.input[0]
262
+ var nextNewline = tmp + skip;
263
+ var ptr = 0;
264
+ while (idx > nextNewline && tmp !== -1) {
265
+ this.incrementLine(1);
266
+ idx -= nextNewline + 1;
267
+ ptr += nextNewline + 1;
268
+ tmp = nextNewline = this.input.substr(ptr).indexOf('\n');
269
+ }
270
+
271
+ this.incrementColumn(idx);
272
+ }
273
+ if (ex.code === 'CHARACTER_PARSER:END_OF_STRING_REACHED') {
274
+ this.error(
275
+ 'NO_END_BRACKET',
276
+ 'The end of the string reached with no closing bracket ' +
277
+ end +
278
+ ' found.'
279
+ );
280
+ } else if (ex.code === 'CHARACTER_PARSER:MISMATCHED_BRACKET') {
281
+ this.error('BRACKET_MISMATCH', ex.message);
282
+ }
283
+ throw ex;
284
+ }
285
+ return range;
286
+ },
287
+
288
+ scanIndentation: function() {
289
+ var captures, re;
290
+
291
+ // established regexp
292
+ if (this.indentRe) {
293
+ captures = this.indentRe.exec(this.input);
294
+ // determine regexp
295
+ } else {
296
+ // tabs
297
+ re = /^\n(\t*) */;
298
+ captures = re.exec(this.input);
299
+
300
+ // spaces
301
+ if (captures && !captures[1].length) {
302
+ re = /^\n( *)/;
303
+ captures = re.exec(this.input);
304
+ }
305
+
306
+ // established
307
+ if (captures && captures[1].length) this.indentRe = re;
308
+ }
309
+
310
+ return captures;
311
+ },
312
+
313
+ /**
314
+ * end-of-source.
315
+ */
316
+
317
+ eos: function() {
318
+ if (this.input.length) return;
319
+ if (this.interpolated) {
320
+ this.error(
321
+ 'NO_END_BRACKET',
322
+ 'End of line was reached with no closing bracket for interpolation.'
323
+ );
324
+ }
325
+ for (var i = 0; this.indentStack[i]; i++) {
326
+ this.tokens.push(this.tokEnd(this.tok('outdent')));
327
+ }
328
+ this.tokens.push(this.tokEnd(this.tok('eos')));
329
+ this.ended = true;
330
+ return true;
331
+ },
332
+
333
+ /**
334
+ * Blank line.
335
+ */
336
+
337
+ blank: function() {
338
+ var captures;
339
+ if ((captures = /^\n[ \t]*\n/.exec(this.input))) {
340
+ this.consume(captures[0].length - 1);
341
+ this.incrementLine(1);
342
+ return true;
343
+ }
344
+ },
345
+
346
+ /**
347
+ * Comment.
348
+ */
349
+
350
+ comment: function() {
351
+ var captures;
352
+ if ((captures = /^\/\/(-)?([^\n]*)/.exec(this.input))) {
353
+ this.consume(captures[0].length);
354
+ var tok = this.tok('comment', captures[2]);
355
+ tok.buffer = '-' != captures[1];
356
+ this.interpolationAllowed = tok.buffer;
357
+ this.tokens.push(tok);
358
+ this.incrementColumn(captures[0].length);
359
+ this.tokEnd(tok);
360
+ this.callLexerFunction('pipelessText');
361
+ return true;
362
+ }
363
+ },
364
+
365
+ /**
366
+ * Interpolated tag.
367
+ */
368
+
369
+ interpolation: function() {
370
+ if (/^#\{/.test(this.input)) {
371
+ var match = this.bracketExpression(1);
372
+ this.consume(match.end + 1);
373
+ var tok = this.tok('interpolation', match.src);
374
+ this.tokens.push(tok);
375
+ this.incrementColumn(2); // '#{'
376
+ this.assertExpression(match.src);
377
+
378
+ var splitted = match.src.split('\n');
379
+ var lines = splitted.length - 1;
380
+ this.incrementLine(lines);
381
+ this.incrementColumn(splitted[lines].length + 1); // + 1 → '}'
382
+ this.tokEnd(tok);
383
+ return true;
384
+ }
385
+ },
386
+
387
+ /**
388
+ * Tag.
389
+ */
390
+
391
+ tag: function() {
392
+ var captures;
393
+
394
+ if ((captures = /^(\w(?:[-:\w]*\w)?)/.exec(this.input))) {
395
+ var tok,
396
+ name = captures[1],
397
+ len = captures[0].length;
398
+ this.consume(len);
399
+ tok = this.tok('tag', name);
400
+ this.tokens.push(tok);
401
+ this.incrementColumn(len);
402
+ this.tokEnd(tok);
403
+ return true;
404
+ }
405
+ },
406
+
407
+ /**
408
+ * Filter.
409
+ */
410
+
411
+ filter: function(opts) {
412
+ var tok = this.scan(/^:([\w\-]+)/, 'filter');
413
+ var inInclude = opts && opts.inInclude;
414
+ if (tok) {
415
+ this.tokens.push(tok);
416
+ this.incrementColumn(tok.val.length);
417
+ this.tokEnd(tok);
418
+ this.callLexerFunction('attrs');
419
+ if (!inInclude) {
420
+ this.interpolationAllowed = false;
421
+ this.callLexerFunction('pipelessText');
422
+ }
423
+ return true;
424
+ }
425
+ },
426
+
427
+ /**
428
+ * Doctype.
429
+ */
430
+
431
+ doctype: function() {
432
+ var node = this.scanEndOfLine(/^doctype *([^\n]*)/, 'doctype');
433
+ if (node) {
434
+ this.tokens.push(this.tokEnd(node));
435
+ return true;
436
+ }
437
+ },
438
+
439
+ /**
440
+ * Id.
441
+ */
442
+
443
+ id: function() {
444
+ var tok = this.scan(/^#([\w-]+)/, 'id');
445
+ if (tok) {
446
+ this.tokens.push(tok);
447
+ this.incrementColumn(tok.val.length);
448
+ this.tokEnd(tok);
449
+ return true;
450
+ }
451
+ if (/^#/.test(this.input)) {
452
+ this.error(
453
+ 'INVALID_ID',
454
+ '"' +
455
+ /.[^ \t\(\#\.\:]*/.exec(this.input.substr(1))[0] +
456
+ '" is not a valid ID.'
457
+ );
458
+ }
459
+ },
460
+
461
+ /**
462
+ * Class.
463
+ */
464
+
465
+ className: function() {
466
+ var tok = this.scan(/^\.([_a-z0-9\-]*[_a-z][_a-z0-9\-]*)/i, 'class');
467
+ if (tok) {
468
+ this.tokens.push(tok);
469
+ this.incrementColumn(tok.val.length);
470
+ this.tokEnd(tok);
471
+ return true;
472
+ }
473
+ if (/^\.[_a-z0-9\-]+/i.test(this.input)) {
474
+ this.error(
475
+ 'INVALID_CLASS_NAME',
476
+ 'Class names must contain at least one letter or underscore.'
477
+ );
478
+ }
479
+ if (/^\./.test(this.input)) {
480
+ this.error(
481
+ 'INVALID_CLASS_NAME',
482
+ '"' +
483
+ /.[^ \t\(\#\.\:]*/.exec(this.input.substr(1))[0] +
484
+ '" is not a valid class name. Class names can only contain "_", "-", a-z and 0-9, and must contain at least one of "_", or a-z'
485
+ );
486
+ }
487
+ },
488
+
489
+ /**
490
+ * Text.
491
+ */
492
+ endInterpolation: function() {
493
+ if (this.interpolated && this.input[0] === ']') {
494
+ this.input = this.input.substr(1);
495
+ this.ended = true;
496
+ return true;
497
+ }
498
+ },
499
+ addText: function(type, value, prefix, escaped) {
500
+ var tok;
501
+ if (value + prefix === '') return;
502
+ prefix = prefix || '';
503
+ escaped = escaped || 0;
504
+ var indexOfEnd = this.interpolated ? value.indexOf(']') : -1;
505
+ var indexOfStart = this.interpolationAllowed ? value.indexOf('#[') : -1;
506
+ var indexOfEscaped = this.interpolationAllowed ? value.indexOf('\\#[') : -1;
507
+ var matchOfStringInterp = /(\\)?([!]){((?:.|\n)*)$/.exec(value);
508
+ var indexOfStringInterp =
509
+ this.interpolationAllowed && matchOfStringInterp
510
+ ? matchOfStringInterp.index
511
+ : Infinity;
512
+
513
+ if (indexOfEnd === -1) indexOfEnd = Infinity;
514
+ if (indexOfStart === -1) indexOfStart = Infinity;
515
+ if (indexOfEscaped === -1) indexOfEscaped = Infinity;
516
+
517
+ if (
518
+ indexOfEscaped !== Infinity &&
519
+ indexOfEscaped < indexOfEnd &&
520
+ indexOfEscaped < indexOfStart &&
521
+ indexOfEscaped < indexOfStringInterp
522
+ ) {
523
+ prefix = prefix + value.substring(0, indexOfEscaped) + '#[';
524
+ return this.addText(
525
+ type,
526
+ value.substring(indexOfEscaped + 3),
527
+ prefix,
528
+ escaped + 1
529
+ );
530
+ }
531
+ if (
532
+ indexOfStart !== Infinity &&
533
+ indexOfStart < indexOfEnd &&
534
+ indexOfStart < indexOfEscaped &&
535
+ indexOfStart < indexOfStringInterp
536
+ ) {
537
+ tok = this.tok(type, prefix + value.substring(0, indexOfStart));
538
+ this.incrementColumn(prefix.length + indexOfStart + escaped);
539
+ this.tokens.push(this.tokEnd(tok));
540
+ tok = this.tok('start-pug-interpolation');
541
+ this.incrementColumn(2);
542
+ this.tokens.push(this.tokEnd(tok));
543
+ var child = new this.constructor(value.substr(indexOfStart + 2), {
544
+ filename: this.filename,
545
+ interpolated: true,
546
+ startingLine: this.lineno,
547
+ startingColumn: this.colno,
548
+ plugins: this.plugins,
549
+ });
550
+ var interpolated;
551
+ try {
552
+ interpolated = child.getTokens();
553
+ } catch (ex) {
554
+ if (ex.code && /^PUG:/.test(ex.code)) {
555
+ this.colno = ex.column;
556
+ this.error(ex.code.substr(4), ex.msg);
557
+ }
558
+ throw ex;
559
+ }
560
+ this.colno = child.colno;
561
+ this.tokens = this.tokens.concat(interpolated);
562
+ tok = this.tok('end-pug-interpolation');
563
+ this.incrementColumn(1);
564
+ this.tokens.push(this.tokEnd(tok));
565
+ this.addText(type, child.input);
566
+ return;
567
+ }
568
+ if (
569
+ indexOfEnd !== Infinity &&
570
+ indexOfEnd < indexOfStart &&
571
+ indexOfEnd < indexOfEscaped &&
572
+ indexOfEnd < indexOfStringInterp
573
+ ) {
574
+ if (prefix + value.substring(0, indexOfEnd)) {
575
+ this.addText(type, value.substring(0, indexOfEnd), prefix);
576
+ }
577
+ this.ended = true;
578
+ this.input = value.substr(value.indexOf(']') + 1) + this.input;
579
+ return;
580
+ }
581
+ if (indexOfStringInterp !== Infinity) {
582
+ if (matchOfStringInterp[1]) {
583
+ prefix =
584
+ prefix +
585
+ value.substring(0, indexOfStringInterp) +
586
+ matchOfStringInterp[2] +
587
+ '{';
588
+ return this.addText(
589
+ type,
590
+ value.substring(indexOfStringInterp + 3),
591
+ prefix,
592
+ escaped + 1
593
+ );
594
+ }
595
+ var before = value.substr(0, indexOfStringInterp);
596
+ if (prefix || before) {
597
+ before = prefix + before;
598
+ tok = this.tok(type, before);
599
+ this.incrementColumn(before.length + escaped);
600
+ this.tokens.push(this.tokEnd(tok));
601
+ }
602
+
603
+ var rest = matchOfStringInterp[3];
604
+ var range;
605
+ tok = this.tok('interpolated-code');
606
+ this.incrementColumn(2);
607
+ try {
608
+ range = characterParser.parseUntil(rest, '}');
609
+ } catch (ex) {
610
+ if (ex.index !== undefined) {
611
+ this.incrementColumn(ex.index);
612
+ }
613
+ if (ex.code === 'CHARACTER_PARSER:END_OF_STRING_REACHED') {
614
+ this.error(
615
+ 'NO_END_BRACKET',
616
+ 'End of line was reached with no closing bracket for interpolation.'
617
+ );
618
+ } else if (ex.code === 'CHARACTER_PARSER:MISMATCHED_BRACKET') {
619
+ this.error('BRACKET_MISMATCH', ex.message);
620
+ } else {
621
+ throw ex;
622
+ }
623
+ }
624
+ tok.mustEscape = matchOfStringInterp[2] === '#';
625
+ tok.buffer = true;
626
+ tok.val = range.src;
627
+ this.assertExpression(range.src);
628
+
629
+ if (range.end + 1 < rest.length) {
630
+ rest = rest.substr(range.end + 1);
631
+ this.incrementColumn(range.end + 1);
632
+ this.tokens.push(this.tokEnd(tok));
633
+ this.addText(type, rest);
634
+ } else {
635
+ this.incrementColumn(rest.length);
636
+ this.tokens.push(this.tokEnd(tok));
637
+ }
638
+ return;
639
+ }
640
+
641
+ value = prefix + value;
642
+ tok = this.tok(type, value);
643
+ this.incrementColumn(value.length + escaped);
644
+ this.tokens.push(this.tokEnd(tok));
645
+ },
646
+
647
+ text: function() {
648
+ var tok =
649
+ this.scan(/^(?:\| ?| )([^\n]+)/, 'text') ||
650
+ this.scan(/^( )/, 'text') ||
651
+ this.scan(/^\|( ?)/, 'text');
652
+ if (tok) {
653
+ this.addText('text', tok.val);
654
+ return true;
655
+ }
656
+ },
657
+
658
+ textHtml: function() {
659
+ var tok = this.scan(/^(<[^\n]*)/, 'text-html');
660
+ if (tok) {
661
+ this.addText('text-html', tok.val);
662
+ return true;
663
+ }
664
+ },
665
+
666
+ /**
667
+ * Dot.
668
+ */
669
+
670
+ dot: function() {
671
+ var tok;
672
+ if ((tok = this.scanEndOfLine(/^\./, 'dot'))) {
673
+ this.tokens.push(this.tokEnd(tok));
674
+ this.callLexerFunction('pipelessText');
675
+ return true;
676
+ }
677
+ },
678
+
679
+ /**
680
+ * Extends.
681
+ */
682
+
683
+ extends: function() {
684
+ var tok = this.scan(/^extends?(?= |$|\n)/, 'extends');
685
+ if (tok) {
686
+ this.tokens.push(this.tokEnd(tok));
687
+ if (!this.callLexerFunction('path')) {
688
+ this.error('NO_EXTENDS_PATH', 'missing path for extends');
689
+ }
690
+ return true;
691
+ }
692
+ if (this.scan(/^extends?\b/)) {
693
+ this.error('MALFORMED_EXTENDS', 'malformed extends');
694
+ }
695
+ },
696
+
697
+ /**
698
+ * Block prepend.
699
+ */
700
+
701
+ prepend: function() {
702
+ var captures;
703
+ if ((captures = /^(?:block +)?prepend +([^\n]+)/.exec(this.input))) {
704
+ var name = captures[1].trim();
705
+ var comment = '';
706
+ if (name.indexOf('//') !== -1) {
707
+ comment =
708
+ '//' +
709
+ name
710
+ .split('//')
711
+ .slice(1)
712
+ .join('//');
713
+ name = name.split('//')[0].trim();
714
+ }
715
+ if (!name) return;
716
+ var tok = this.tok('block', name);
717
+ var len = captures[0].length - comment.length;
718
+ while (this.whitespaceRe.test(this.input.charAt(len - 1))) len--;
719
+ this.incrementColumn(len);
720
+ tok.mode = 'prepend';
721
+ this.tokens.push(this.tokEnd(tok));
722
+ this.consume(captures[0].length - comment.length);
723
+ this.incrementColumn(captures[0].length - comment.length - len);
724
+ return true;
725
+ }
726
+ },
727
+
728
+ /**
729
+ * Block append.
730
+ */
731
+
732
+ append: function() {
733
+ var captures;
734
+ if ((captures = /^(?:block +)?append +([^\n]+)/.exec(this.input))) {
735
+ var name = captures[1].trim();
736
+ var comment = '';
737
+ if (name.indexOf('//') !== -1) {
738
+ comment =
739
+ '//' +
740
+ name
741
+ .split('//')
742
+ .slice(1)
743
+ .join('//');
744
+ name = name.split('//')[0].trim();
745
+ }
746
+ if (!name) return;
747
+ var tok = this.tok('block', name);
748
+ var len = captures[0].length - comment.length;
749
+ while (this.whitespaceRe.test(this.input.charAt(len - 1))) len--;
750
+ this.incrementColumn(len);
751
+ tok.mode = 'append';
752
+ this.tokens.push(this.tokEnd(tok));
753
+ this.consume(captures[0].length - comment.length);
754
+ this.incrementColumn(captures[0].length - comment.length - len);
755
+ return true;
756
+ }
757
+ },
758
+
759
+ /**
760
+ * Block.
761
+ */
762
+
763
+ block: function() {
764
+ var captures;
765
+ if ((captures = /^block +([^\n]+)/.exec(this.input))) {
766
+ var name = captures[1].trim();
767
+ var comment = '';
768
+ if (name.indexOf('//') !== -1) {
769
+ comment =
770
+ '//' +
771
+ name
772
+ .split('//')
773
+ .slice(1)
774
+ .join('//');
775
+ name = name.split('//')[0].trim();
776
+ }
777
+ if (!name) return;
778
+ var tok = this.tok('block', name);
779
+ var len = captures[0].length - comment.length;
780
+ while (this.whitespaceRe.test(this.input.charAt(len - 1))) len--;
781
+ this.incrementColumn(len);
782
+ tok.mode = 'replace';
783
+ this.tokens.push(this.tokEnd(tok));
784
+ this.consume(captures[0].length - comment.length);
785
+ this.incrementColumn(captures[0].length - comment.length - len);
786
+ return true;
787
+ }
788
+ },
789
+
790
+ /**
791
+ * Mixin Block.
792
+ */
793
+
794
+ mixinBlock: function() {
795
+ var tok;
796
+ if ((tok = this.scanEndOfLine(/^block/, 'mixin-block'))) {
797
+ this.tokens.push(this.tokEnd(tok));
798
+ return true;
799
+ }
800
+ },
801
+
802
+ /**
803
+ * Yield.
804
+ */
805
+
806
+ yield: function() {
807
+ var tok = this.scanEndOfLine(/^yield/, 'yield');
808
+ if (tok) {
809
+ this.tokens.push(this.tokEnd(tok));
810
+ return true;
811
+ }
812
+ },
813
+
814
+ /**
815
+ * Include.
816
+ */
817
+
818
+ include: function() {
819
+ var tok = this.scan(/^include(?=:| |$|\n)/, 'include');
820
+ if (tok) {
821
+ this.tokens.push(this.tokEnd(tok));
822
+ while (this.callLexerFunction('filter', {inInclude: true}));
823
+ if (!this.callLexerFunction('path')) {
824
+ if (/^[^ \n]+/.test(this.input)) {
825
+ // if there is more text
826
+ this.fail();
827
+ } else {
828
+ // if not
829
+ this.error('NO_INCLUDE_PATH', 'missing path for include');
830
+ }
831
+ }
832
+ return true;
833
+ }
834
+ if (this.scan(/^include\b/)) {
835
+ this.error('MALFORMED_INCLUDE', 'malformed include');
836
+ }
837
+ },
838
+
839
+ /**
840
+ * Path
841
+ */
842
+
843
+ path: function() {
844
+ var tok = this.scanEndOfLine(/^ ([^\n]+)/, 'path');
845
+ if (tok && (tok.val = tok.val.trim())) {
846
+ this.tokens.push(this.tokEnd(tok));
847
+ return true;
848
+ }
849
+ },
850
+
851
+ /**
852
+ * Case.
853
+ */
854
+
855
+ case: function() {
856
+ var tok = this.scanEndOfLine(/^case +([^\n]+)/, 'case');
857
+ if (tok) {
858
+ this.incrementColumn(-tok.val.length);
859
+ this.assertExpression(tok.val);
860
+ this.incrementColumn(tok.val.length);
861
+ this.tokens.push(this.tokEnd(tok));
862
+ return true;
863
+ }
864
+ if (this.scan(/^case\b/)) {
865
+ this.error('NO_CASE_EXPRESSION', 'missing expression for case');
866
+ }
867
+ },
868
+
869
+ /**
870
+ * When.
871
+ */
872
+
873
+ when: function() {
874
+ var tok = this.scanEndOfLine(/^when +([^:\n]+)/, 'when');
875
+ if (tok) {
876
+ var parser = characterParser.default(tok.val);
877
+ while (parser.isNesting() || parser.isString()) {
878
+ var rest = /:([^:\n]+)/.exec(this.input);
879
+ if (!rest) break;
880
+
881
+ tok.val += rest[0];
882
+ this.consume(rest[0].length);
883
+ this.incrementColumn(rest[0].length);
884
+ parser = characterParser.default(tok.val);
885
+ }
886
+
887
+ this.incrementColumn(-tok.val.length);
888
+ this.assertExpression(tok.val);
889
+ this.incrementColumn(tok.val.length);
890
+ this.tokens.push(this.tokEnd(tok));
891
+ return true;
892
+ }
893
+ if (this.scan(/^when\b/)) {
894
+ this.error('NO_WHEN_EXPRESSION', 'missing expression for when');
895
+ }
896
+ },
897
+
898
+ /**
899
+ * Default.
900
+ */
901
+
902
+ default: function() {
903
+ var tok = this.scanEndOfLine(/^default/, 'default');
904
+ if (tok) {
905
+ this.tokens.push(this.tokEnd(tok));
906
+ return true;
907
+ }
908
+ if (this.scan(/^default\b/)) {
909
+ this.error(
910
+ 'DEFAULT_WITH_EXPRESSION',
911
+ 'default should not have an expression'
912
+ );
913
+ }
914
+ },
915
+
916
+ /**
917
+ * Call mixin.
918
+ */
919
+
920
+ call: function() {
921
+ var tok, captures, increment;
922
+ if ((captures = /^\+(\s*)(([-\w]+)|(#\{))/.exec(this.input))) {
923
+ // try to consume simple or interpolated call
924
+ if (captures[3]) {
925
+ // simple call
926
+ increment = captures[0].length;
927
+ this.consume(increment);
928
+ tok = this.tok('call', captures[3]);
929
+ } else {
930
+ // interpolated call
931
+ var match = this.bracketExpression(2 + captures[1].length);
932
+ increment = match.end + 1;
933
+ this.consume(increment);
934
+ this.assertExpression(match.src);
935
+ tok = this.tok('call', '#{' + match.src + '}');
936
+ }
937
+
938
+ this.incrementColumn(increment);
939
+
940
+ tok.args = null;
941
+ // Check for args (not attributes)
942
+ if ((captures = /^ *\(/.exec(this.input))) {
943
+ var range = this.bracketExpression(captures[0].length - 1);
944
+ if (!/^\s*[-\w]+ *=/.test(range.src)) {
945
+ // not attributes
946
+ this.incrementColumn(1);
947
+ this.consume(range.end + 1);
948
+ tok.args = range.src;
949
+ this.assertExpression('[' + tok.args + ']');
950
+ for (var i = 0; i <= tok.args.length; i++) {
951
+ if (tok.args[i] === '\n') {
952
+ this.incrementLine(1);
953
+ } else {
954
+ this.incrementColumn(1);
955
+ }
956
+ }
957
+ }
958
+ }
959
+ this.tokens.push(this.tokEnd(tok));
960
+ return true;
961
+ }
962
+ },
963
+
964
+ /**
965
+ * Mixin.
966
+ */
967
+
968
+ mixin: function() {
969
+ var captures;
970
+ if ((captures = /^mixin +([-\w]+)(?: *\((.*)\))? */.exec(this.input))) {
971
+ this.consume(captures[0].length);
972
+ var tok = this.tok('mixin', captures[1]);
973
+ tok.args = captures[2] || null;
974
+ this.incrementColumn(captures[0].length);
975
+ this.tokens.push(this.tokEnd(tok));
976
+ return true;
977
+ }
978
+ },
979
+
980
+ /**
981
+ * Conditional.
982
+ */
983
+
984
+ conditional: function() {
985
+ var captures;
986
+ if ((captures = /^(if|unless|else if|else)\b([^\n]*)/.exec(this.input))) {
987
+ this.consume(captures[0].length);
988
+ var type = captures[1].replace(/ /g, '-');
989
+ var js = captures[2] && captures[2].trim();
990
+ // type can be "if", "else-if" and "else"
991
+ var tok = this.tok(type, js);
992
+ this.incrementColumn(captures[0].length - js.length);
993
+
994
+ switch (type) {
995
+ case 'if':
996
+ case 'else-if':
997
+ this.assertExpression(js);
998
+ break;
999
+ case 'unless':
1000
+ this.assertExpression(js);
1001
+ tok.val = '!(' + js + ')';
1002
+ tok.type = 'if';
1003
+ break;
1004
+ case 'else':
1005
+ if (js) {
1006
+ this.error(
1007
+ 'ELSE_CONDITION',
1008
+ '`else` cannot have a condition, perhaps you meant `else if`'
1009
+ );
1010
+ }
1011
+ break;
1012
+ }
1013
+ this.incrementColumn(js.length);
1014
+ this.tokens.push(this.tokEnd(tok));
1015
+ return true;
1016
+ }
1017
+ },
1018
+
1019
+ /**
1020
+ * While.
1021
+ */
1022
+
1023
+ while: function() {
1024
+ var captures, tok;
1025
+ if ((captures = /^while +([^\n]+)/.exec(this.input))) {
1026
+ this.consume(captures[0].length);
1027
+ this.assertExpression(captures[1]);
1028
+ tok = this.tok('while', captures[1]);
1029
+ this.incrementColumn(captures[0].length);
1030
+ this.tokens.push(this.tokEnd(tok));
1031
+ return true;
1032
+ }
1033
+ if (this.scan(/^while\b/)) {
1034
+ this.error('NO_WHILE_EXPRESSION', 'missing expression for while');
1035
+ }
1036
+ },
1037
+
1038
+ /**
1039
+ * Each.
1040
+ */
1041
+
1042
+ each: function() {
1043
+ var captures;
1044
+ if (
1045
+ (captures = /^(?:each|for) +([a-zA-Z_$][\w$]*)(?: *, *([a-zA-Z_$][\w$]*))? * in *([^\n]+)/.exec(
1046
+ this.input
1047
+ ))
1048
+ ) {
1049
+ this.consume(captures[0].length);
1050
+ var tok = this.tok('each', captures[1]);
1051
+ tok.key = captures[2] || null;
1052
+ this.incrementColumn(captures[0].length - captures[3].length);
1053
+ this.assertExpression(captures[3]);
1054
+ tok.code = captures[3];
1055
+ this.incrementColumn(captures[3].length);
1056
+ this.tokens.push(this.tokEnd(tok));
1057
+ return true;
1058
+ }
1059
+ const name = /^each\b/.exec(this.input) ? 'each' : 'for';
1060
+ if (this.scan(/^(?:each|for)\b/)) {
1061
+ this.error(
1062
+ 'MALFORMED_EACH',
1063
+ 'This `' +
1064
+ name +
1065
+ '` has a syntax error. `' +
1066
+ name +
1067
+ '` statements should be of the form: `' +
1068
+ name +
1069
+ ' VARIABLE_NAME of JS_EXPRESSION`'
1070
+ );
1071
+ }
1072
+ if (
1073
+ (captures = /^- *(?:each|for) +([a-zA-Z_$][\w$]*)(?: *, *([a-zA-Z_$][\w$]*))? +in +([^\n]+)/.exec(
1074
+ this.input
1075
+ ))
1076
+ ) {
1077
+ this.error(
1078
+ 'MALFORMED_EACH',
1079
+ 'Pug each and for should no longer be prefixed with a dash ("-"). They are pug keywords and not part of JavaScript.'
1080
+ );
1081
+ }
1082
+ },
1083
+
1084
+ /**
1085
+ * EachOf.
1086
+ */
1087
+
1088
+ eachOf: function() {
1089
+ var captures;
1090
+ if ((captures = /^(?:each|for) (.*?) of *([^\n]+)/.exec(this.input))) {
1091
+ this.consume(captures[0].length);
1092
+ var tok = this.tok('eachOf', captures[1]);
1093
+ tok.value = captures[1];
1094
+ this.incrementColumn(captures[0].length - captures[2].length);
1095
+ this.assertExpression(captures[2]);
1096
+ tok.code = captures[2];
1097
+ this.incrementColumn(captures[2].length);
1098
+ this.tokens.push(this.tokEnd(tok));
1099
+
1100
+ if (
1101
+ !(
1102
+ /^[a-zA-Z_$][\w$]*$/.test(tok.value.trim()) ||
1103
+ /^\[ *[a-zA-Z_$][\w$]* *\, *[a-zA-Z_$][\w$]* *\]$/.test(
1104
+ tok.value.trim()
1105
+ )
1106
+ )
1107
+ ) {
1108
+ this.error(
1109
+ 'MALFORMED_EACH_OF_LVAL',
1110
+ 'The value variable for each must either be a valid identifier (e.g. `item`) or a pair of identifiers in square brackets (e.g. `[key, value]`).'
1111
+ );
1112
+ }
1113
+
1114
+ return true;
1115
+ }
1116
+ if (
1117
+ (captures = /^- *(?:each|for) +([a-zA-Z_$][\w$]*)(?: *, *([a-zA-Z_$][\w$]*))? +of +([^\n]+)/.exec(
1118
+ this.input
1119
+ ))
1120
+ ) {
1121
+ this.error(
1122
+ 'MALFORMED_EACH',
1123
+ 'Pug each and for should not be prefixed with a dash ("-"). They are pug keywords and not part of JavaScript.'
1124
+ );
1125
+ }
1126
+ },
1127
+
1128
+ /**
1129
+ * Code.
1130
+ */
1131
+
1132
+ code: function() {
1133
+ var captures;
1134
+ if ((captures = /^(!?=|-)[ \t]*([^\n]+)/.exec(this.input))) {
1135
+ var flags = captures[1];
1136
+ var code = captures[2];
1137
+ var shortened = 0;
1138
+ if (this.interpolated) {
1139
+ var parsed;
1140
+ try {
1141
+ parsed = characterParser.parseUntil(code, ']');
1142
+ } catch (err) {
1143
+ if (err.index !== undefined) {
1144
+ this.incrementColumn(captures[0].length - code.length + err.index);
1145
+ }
1146
+ if (err.code === 'CHARACTER_PARSER:END_OF_STRING_REACHED') {
1147
+ this.error(
1148
+ 'NO_END_BRACKET',
1149
+ 'End of line was reached with no closing bracket for interpolation.'
1150
+ );
1151
+ } else if (err.code === 'CHARACTER_PARSER:MISMATCHED_BRACKET') {
1152
+ this.error('BRACKET_MISMATCH', err.message);
1153
+ } else {
1154
+ throw err;
1155
+ }
1156
+ }
1157
+ shortened = code.length - parsed.end;
1158
+ code = parsed.src;
1159
+ }
1160
+ var consumed = captures[0].length - shortened;
1161
+ this.consume(consumed);
1162
+ var tok = this.tok('code', code);
1163
+ tok.mustEscape = flags.charAt(0) === '=';
1164
+ tok.buffer = flags.charAt(0) === '=' || flags.charAt(1) === '=';
1165
+
1166
+ // p #[!= abc] hey
1167
+ // ^ original colno
1168
+ // -------------- captures[0]
1169
+ // -------- captures[2]
1170
+ // ------ captures[0] - captures[2]
1171
+ // ^ after colno
1172
+
1173
+ // = abc
1174
+ // ^ original colno
1175
+ // ------- captures[0]
1176
+ // --- captures[2]
1177
+ // ---- captures[0] - captures[2]
1178
+ // ^ after colno
1179
+ this.incrementColumn(captures[0].length - captures[2].length);
1180
+ if (tok.buffer) this.assertExpression(code);
1181
+ this.tokens.push(tok);
1182
+
1183
+ // p #[!= abc] hey
1184
+ // ^ original colno
1185
+ // ----- shortened
1186
+ // --- code
1187
+ // ^ after colno
1188
+
1189
+ // = abc
1190
+ // ^ original colno
1191
+ // shortened
1192
+ // --- code
1193
+ // ^ after colno
1194
+ this.incrementColumn(code.length);
1195
+ this.tokEnd(tok);
1196
+ return true;
1197
+ }
1198
+ },
1199
+
1200
+ /**
1201
+ * Block code.
1202
+ */
1203
+ blockCode: function() {
1204
+ var tok;
1205
+ if ((tok = this.scanEndOfLine(/^-/, 'blockcode'))) {
1206
+ this.tokens.push(this.tokEnd(tok));
1207
+ this.interpolationAllowed = false;
1208
+ this.callLexerFunction('pipelessText');
1209
+ return true;
1210
+ }
1211
+ },
1212
+
1213
+ /**
1214
+ * Attribute Name.
1215
+ */
1216
+ attribute: function(str) {
1217
+ var quote = '';
1218
+ var quoteRe = /['"]/;
1219
+ var key = '';
1220
+ var i;
1221
+
1222
+ // consume all whitespace before the key
1223
+ for (i = 0; i < str.length; i++) {
1224
+ if (!this.whitespaceRe.test(str[i])) break;
1225
+ if (str[i] === '\n') {
1226
+ this.incrementLine(1);
1227
+ } else {
1228
+ this.incrementColumn(1);
1229
+ }
1230
+ }
1231
+
1232
+ if (i === str.length) {
1233
+ return '';
1234
+ }
1235
+
1236
+ var tok = this.tok('attribute');
1237
+
1238
+ // quote?
1239
+ if (quoteRe.test(str[i])) {
1240
+ quote = str[i];
1241
+ this.incrementColumn(1);
1242
+ i++;
1243
+ }
1244
+
1245
+ // start looping through the key
1246
+ for (; i < str.length; i++) {
1247
+ if (quote) {
1248
+ if (str[i] === quote) {
1249
+ this.incrementColumn(1);
1250
+ i++;
1251
+ break;
1252
+ }
1253
+ } else {
1254
+ if (
1255
+ this.whitespaceRe.test(str[i]) ||
1256
+ str[i] === '!' ||
1257
+ str[i] === '=' ||
1258
+ str[i] === ','
1259
+ ) {
1260
+ break;
1261
+ }
1262
+ }
1263
+
1264
+ key += str[i];
1265
+
1266
+ if (str[i] === '\n') {
1267
+ this.incrementLine(1);
1268
+ } else {
1269
+ this.incrementColumn(1);
1270
+ }
1271
+ }
1272
+
1273
+ tok.name = key;
1274
+
1275
+ var valueResponse = this.attributeValue(str.substr(i));
1276
+
1277
+ if (valueResponse.val) {
1278
+ tok.val = valueResponse.val;
1279
+ tok.mustEscape = valueResponse.mustEscape;
1280
+ } else {
1281
+ // was a boolean attribute (ex: `input(disabled)`)
1282
+ tok.val = true;
1283
+ tok.mustEscape = true;
1284
+ }
1285
+
1286
+ str = valueResponse.remainingSource;
1287
+
1288
+ this.tokens.push(this.tokEnd(tok));
1289
+
1290
+ for (i = 0; i < str.length; i++) {
1291
+ if (!this.whitespaceRe.test(str[i])) {
1292
+ break;
1293
+ }
1294
+ if (str[i] === '\n') {
1295
+ this.incrementLine(1);
1296
+ } else {
1297
+ this.incrementColumn(1);
1298
+ }
1299
+ }
1300
+
1301
+ if (str[i] === ',') {
1302
+ this.incrementColumn(1);
1303
+ i++;
1304
+ }
1305
+
1306
+ return str.substr(i);
1307
+ },
1308
+
1309
+ /**
1310
+ * Attribute Value.
1311
+ */
1312
+ attributeValue: function(str) {
1313
+ var quoteRe = /['"]/;
1314
+ var val = '';
1315
+ var done, i, x;
1316
+ var escapeAttr = true;
1317
+ var state = characterParser.defaultState();
1318
+ var col = this.colno;
1319
+ var line = this.lineno;
1320
+
1321
+ // consume all whitespace before the equals sign
1322
+ for (i = 0; i < str.length; i++) {
1323
+ if (!this.whitespaceRe.test(str[i])) break;
1324
+ if (str[i] === '\n') {
1325
+ line++;
1326
+ col = 1;
1327
+ } else {
1328
+ col++;
1329
+ }
1330
+ }
1331
+
1332
+ if (i === str.length) {
1333
+ return {remainingSource: str};
1334
+ }
1335
+
1336
+ if (str[i] === '!') {
1337
+ escapeAttr = false;
1338
+ col++;
1339
+ i++;
1340
+ if (str[i] !== '=')
1341
+ this.error(
1342
+ 'INVALID_KEY_CHARACTER',
1343
+ 'Unexpected character ' + str[i] + ' expected `=`'
1344
+ );
1345
+ }
1346
+
1347
+ if (str[i] !== '=') {
1348
+ // check for anti-pattern `div("foo"bar)`
1349
+ if (i === 0 && str && !this.whitespaceRe.test(str[0]) && str[0] !== ',') {
1350
+ this.error(
1351
+ 'INVALID_KEY_CHARACTER',
1352
+ 'Unexpected character ' + str[0] + ' expected `=`'
1353
+ );
1354
+ } else {
1355
+ return {remainingSource: str};
1356
+ }
1357
+ }
1358
+
1359
+ this.lineno = line;
1360
+ this.colno = col + 1;
1361
+ i++;
1362
+
1363
+ // consume all whitespace before the value
1364
+ for (; i < str.length; i++) {
1365
+ if (!this.whitespaceRe.test(str[i])) break;
1366
+ if (str[i] === '\n') {
1367
+ this.incrementLine(1);
1368
+ } else {
1369
+ this.incrementColumn(1);
1370
+ }
1371
+ }
1372
+
1373
+ line = this.lineno;
1374
+ col = this.colno;
1375
+
1376
+ // start looping through the value
1377
+ for (; i < str.length; i++) {
1378
+ // if the character is in a string or in parentheses/brackets/braces
1379
+ if (!(state.isNesting() || state.isString())) {
1380
+ if (this.whitespaceRe.test(str[i])) {
1381
+ done = false;
1382
+
1383
+ // find the first non-whitespace character
1384
+ for (x = i; x < str.length; x++) {
1385
+ if (!this.whitespaceRe.test(str[x])) {
1386
+ // if it is a JavaScript punctuator, then assume that it is
1387
+ // a part of the value
1388
+ const isNotPunctuator = !characterParser.isPunctuator(str[x]);
1389
+ const isQuote = quoteRe.test(str[x]);
1390
+ const isColon = str[x] === ':';
1391
+ const isSpreadOperator =
1392
+ str[x] + str[x + 1] + str[x + 2] === '...';
1393
+ const isAngularBinding =
1394
+ (str[x] === '[' || str[x] === '(' || str[x] === '*') &&
1395
+ /^(\[\(|[\[(*])[a-zA-Z]/.test(str.substr(x));
1396
+ if (
1397
+ (isNotPunctuator || isQuote || isColon || isSpreadOperator || isAngularBinding) &&
1398
+ !characterParser.default(val).isNesting()
1399
+ ) {
1400
+ done = true;
1401
+ }
1402
+ break;
1403
+ }
1404
+ }
1405
+
1406
+ // if everything else is whitespace, return now so last attribute
1407
+ // does not include trailing whitespace
1408
+ if (done || x === str.length) {
1409
+ break;
1410
+ }
1411
+ }
1412
+
1413
+ // if there's no whitespace and the character is not ',', the
1414
+ // attribute did not end.
1415
+ if (str[i] === ',' && !characterParser.default(val).isNesting()) {
1416
+ break;
1417
+ }
1418
+ }
1419
+
1420
+ state = characterParser.parseChar(str[i], state);
1421
+ val += str[i];
1422
+
1423
+ if (str[i] === '\n') {
1424
+ line++;
1425
+ col = 1;
1426
+ } else {
1427
+ col++;
1428
+ }
1429
+ }
1430
+
1431
+ this.assertNestingCorrect(val);
1432
+
1433
+ this.lineno = line;
1434
+ this.colno = col;
1435
+
1436
+ return {val: val, mustEscape: escapeAttr, remainingSource: str.substr(i)};
1437
+ },
1438
+
1439
+ /**
1440
+ * Attributes.
1441
+ */
1442
+
1443
+ attrs: function() {
1444
+ var tok;
1445
+
1446
+ if ('(' == this.input.charAt(0)) {
1447
+ tok = this.tok('start-attributes');
1448
+ var index = this.bracketExpression().end;
1449
+ var str = this.input.substr(1, index - 1);
1450
+
1451
+ this.incrementColumn(1);
1452
+ this.tokens.push(this.tokEnd(tok));
1453
+ this.assertNestingCorrect(str);
1454
+ this.consume(index + 1);
1455
+
1456
+ while (str) {
1457
+ str = this.attribute(str);
1458
+ }
1459
+
1460
+ tok = this.tok('end-attributes');
1461
+ this.incrementColumn(1);
1462
+ this.tokens.push(this.tokEnd(tok));
1463
+ return true;
1464
+ }
1465
+ },
1466
+
1467
+ /**
1468
+ * &attributes block
1469
+ */
1470
+ attributesBlock: function() {
1471
+ if (/^&attributes\b/.test(this.input)) {
1472
+ var consumed = 11;
1473
+ this.consume(consumed);
1474
+ var tok = this.tok('&attributes');
1475
+ this.incrementColumn(consumed);
1476
+ var args = this.bracketExpression();
1477
+ consumed = args.end + 1;
1478
+ this.consume(consumed);
1479
+ tok.val = args.src;
1480
+ this.incrementColumn(consumed);
1481
+ this.tokens.push(this.tokEnd(tok));
1482
+ return true;
1483
+ }
1484
+ },
1485
+
1486
+ /**
1487
+ * Indent | Outdent | Newline.
1488
+ */
1489
+
1490
+ indent: function() {
1491
+ var captures = this.scanIndentation();
1492
+ var tok;
1493
+
1494
+ if (captures) {
1495
+ var indents = captures[1].length;
1496
+
1497
+ this.incrementLine(1);
1498
+ this.consume(indents + 1);
1499
+
1500
+ if (' ' == this.input[0] || '\t' == this.input[0]) {
1501
+ this.error(
1502
+ 'INVALID_INDENTATION',
1503
+ 'Invalid indentation, you can use tabs or spaces but not both'
1504
+ );
1505
+ }
1506
+
1507
+ // blank line
1508
+ if ('\n' == this.input[0]) {
1509
+ this.interpolationAllowed = true;
1510
+ return this.tokEnd(this.tok('newline'));
1511
+ }
1512
+
1513
+ // outdent
1514
+ if (indents < this.indentStack[0]) {
1515
+ var outdent_count = 0;
1516
+ while (this.indentStack[0] > indents) {
1517
+ if (this.indentStack[1] < indents) {
1518
+ this.error(
1519
+ 'INCONSISTENT_INDENTATION',
1520
+ 'Inconsistent indentation. Expecting either ' +
1521
+ this.indentStack[1] +
1522
+ ' or ' +
1523
+ this.indentStack[0] +
1524
+ ' spaces/tabs.'
1525
+ );
1526
+ }
1527
+ outdent_count++;
1528
+ this.indentStack.shift();
1529
+ }
1530
+ while (outdent_count--) {
1531
+ this.colno = 1;
1532
+ tok = this.tok('outdent');
1533
+ this.colno = this.indentStack[0] + 1;
1534
+ this.tokens.push(this.tokEnd(tok));
1535
+ }
1536
+ // indent
1537
+ } else if (indents && indents != this.indentStack[0]) {
1538
+ tok = this.tok('indent', indents);
1539
+ this.colno = 1 + indents;
1540
+ this.tokens.push(this.tokEnd(tok));
1541
+ this.indentStack.unshift(indents);
1542
+ // newline
1543
+ } else {
1544
+ tok = this.tok('newline');
1545
+ this.colno = 1 + Math.min(this.indentStack[0] || 0, indents);
1546
+ this.tokens.push(this.tokEnd(tok));
1547
+ }
1548
+
1549
+ this.interpolationAllowed = true;
1550
+ return true;
1551
+ }
1552
+ },
1553
+
1554
+ pipelessText: function pipelessText(indents) {
1555
+ while (this.callLexerFunction('blank'));
1556
+
1557
+ var captures = this.scanIndentation();
1558
+
1559
+ indents = indents || (captures && captures[1].length);
1560
+ if (indents > this.indentStack[0]) {
1561
+ this.tokens.push(this.tokEnd(this.tok('start-pipeless-text')));
1562
+ var tokens = [];
1563
+ var token_indent = [];
1564
+ var isMatch;
1565
+ // Index in this.input. Can't use this.consume because we might need to
1566
+ // retry lexing the block.
1567
+ var stringPtr = 0;
1568
+ do {
1569
+ // text has `\n` as a prefix
1570
+ var i = this.input.substr(stringPtr + 1).indexOf('\n');
1571
+ if (-1 == i) i = this.input.length - stringPtr - 1;
1572
+ var str = this.input.substr(stringPtr + 1, i);
1573
+ var lineCaptures = this.indentRe.exec('\n' + str);
1574
+ var lineIndents = lineCaptures && lineCaptures[1].length;
1575
+ isMatch = lineIndents >= indents;
1576
+ token_indent.push(isMatch);
1577
+ isMatch = isMatch || !str.trim();
1578
+ if (isMatch) {
1579
+ // consume test along with `\n` prefix if match
1580
+ stringPtr += str.length + 1;
1581
+ tokens.push(str.substr(indents));
1582
+ } else if (lineIndents > this.indentStack[0]) {
1583
+ // line is indented less than the first line but is still indented
1584
+ // need to retry lexing the text block
1585
+ this.tokens.pop();
1586
+ return pipelessText.call(this, lineCaptures[1].length);
1587
+ }
1588
+ } while (this.input.length - stringPtr && isMatch);
1589
+ this.consume(stringPtr);
1590
+ while (this.input.length === 0 && tokens[tokens.length - 1] === '')
1591
+ tokens.pop();
1592
+ tokens.forEach(
1593
+ function(token, i) {
1594
+ var tok;
1595
+ this.incrementLine(1);
1596
+ if (i !== 0) tok = this.tok('newline');
1597
+ if (token_indent[i]) this.incrementColumn(indents);
1598
+ if (tok) this.tokens.push(this.tokEnd(tok));
1599
+ this.addText('text', token);
1600
+ }.bind(this)
1601
+ );
1602
+ this.tokens.push(this.tokEnd(this.tok('end-pipeless-text')));
1603
+ return true;
1604
+ }
1605
+ },
1606
+
1607
+ /**
1608
+ * Slash.
1609
+ */
1610
+
1611
+ slash: function() {
1612
+ var tok = this.scan(/^\//, 'slash');
1613
+ if (tok) {
1614
+ this.tokens.push(this.tokEnd(tok));
1615
+ return true;
1616
+ }
1617
+ },
1618
+
1619
+ /**
1620
+ * ':'
1621
+ */
1622
+
1623
+ colon: function() {
1624
+ var tok = this.scan(/^: +/, ':');
1625
+ if (tok) {
1626
+ this.tokens.push(this.tokEnd(tok));
1627
+ return true;
1628
+ }
1629
+ },
1630
+
1631
+ fail: function() {
1632
+ this.error(
1633
+ 'UNEXPECTED_TEXT',
1634
+ 'unexpected text "' + this.input.substr(0, 5) + '"'
1635
+ );
1636
+ },
1637
+
1638
+ callLexerFunction: function(func) {
1639
+ var rest = [];
1640
+ for (var i = 1; i < arguments.length; i++) {
1641
+ rest.push(arguments[i]);
1642
+ }
1643
+ var pluginArgs = [this].concat(rest);
1644
+ for (var i = 0; i < this.plugins.length; i++) {
1645
+ var plugin = this.plugins[i];
1646
+ if (plugin[func] && plugin[func].apply(plugin, pluginArgs)) {
1647
+ return true;
1648
+ }
1649
+ }
1650
+ return this[func].apply(this, rest);
1651
+ },
1652
+
1653
+ /**
1654
+ * Move to the next token
1655
+ *
1656
+ * @api private
1657
+ */
1658
+
1659
+ advance: function() {
1660
+ return (
1661
+ this.callLexerFunction('blank') ||
1662
+ this.callLexerFunction('eos') ||
1663
+ this.callLexerFunction('endInterpolation') ||
1664
+ this.callLexerFunction('yield') ||
1665
+ this.callLexerFunction('doctype') ||
1666
+ this.callLexerFunction('interpolation') ||
1667
+ this.callLexerFunction('case') ||
1668
+ this.callLexerFunction('when') ||
1669
+ this.callLexerFunction('default') ||
1670
+ this.callLexerFunction('extends') ||
1671
+ this.callLexerFunction('append') ||
1672
+ this.callLexerFunction('prepend') ||
1673
+ this.callLexerFunction('block') ||
1674
+ this.callLexerFunction('mixinBlock') ||
1675
+ this.callLexerFunction('include') ||
1676
+ this.callLexerFunction('mixin') ||
1677
+ this.callLexerFunction('call') ||
1678
+ this.callLexerFunction('conditional') ||
1679
+ this.callLexerFunction('eachOf') ||
1680
+ this.callLexerFunction('each') ||
1681
+ this.callLexerFunction('while') ||
1682
+ this.callLexerFunction('tag') ||
1683
+ this.callLexerFunction('filter') ||
1684
+ this.callLexerFunction('blockCode') ||
1685
+ this.callLexerFunction('code') ||
1686
+ this.callLexerFunction('id') ||
1687
+ this.callLexerFunction('dot') ||
1688
+ this.callLexerFunction('className') ||
1689
+ this.callLexerFunction('attrs') ||
1690
+ this.callLexerFunction('attributesBlock') ||
1691
+ this.callLexerFunction('indent') ||
1692
+ this.callLexerFunction('text') ||
1693
+ this.callLexerFunction('textHtml') ||
1694
+ this.callLexerFunction('comment') ||
1695
+ this.callLexerFunction('slash') ||
1696
+ this.callLexerFunction('colon') ||
1697
+ this.fail()
1698
+ );
1699
+ },
1700
+
1701
+ /**
1702
+ * Return an array of tokens for the current file
1703
+ *
1704
+ * @returns {Array.<Token>}
1705
+ * @api public
1706
+ */
1707
+ getTokens: function() {
1708
+ while (!this.ended) {
1709
+ this.callLexerFunction('advance');
1710
+ }
1711
+ return this.tokens;
1712
+ },
1713
+ };