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/parser.js ADDED
@@ -0,0 +1,1299 @@
1
+ 'use strict';
2
+
3
+ var assert = require('assert');
4
+ var TokenStream = require('token-stream');
5
+ var error = require('./error');
6
+ var inlineTags = require('./parser-lib/inline-tags');
7
+
8
+ module.exports = parse;
9
+ module.exports.Parser = Parser;
10
+ function parse(tokens, options) {
11
+ var parser = new Parser(tokens, options);
12
+ var ast = parser.parse();
13
+ return JSON.parse(JSON.stringify(ast));
14
+ }
15
+
16
+ /**
17
+ * Initialize `Parser` with the given input `str` and `filename`.
18
+ *
19
+ * @param {String} str
20
+ * @param {String} filename
21
+ * @param {Object} options
22
+ * @api public
23
+ */
24
+
25
+ function Parser(tokens, options) {
26
+ options = options || {};
27
+ if (!Array.isArray(tokens)) {
28
+ throw new Error(
29
+ 'Expected tokens to be an Array but got "' + typeof tokens + '"'
30
+ );
31
+ }
32
+ if (typeof options !== 'object') {
33
+ throw new Error(
34
+ 'Expected "options" to be an object but got "' + typeof options + '"'
35
+ );
36
+ }
37
+ this.tokens = new TokenStream(tokens);
38
+ this.filename = options.filename;
39
+ this.src = options.src;
40
+ this.inMixin = 0;
41
+ this.plugins = options.plugins || [];
42
+ }
43
+
44
+ /**
45
+ * Parser prototype.
46
+ */
47
+
48
+ Parser.prototype = {
49
+ /**
50
+ * Save original constructor
51
+ */
52
+
53
+ constructor: Parser,
54
+
55
+ error: function(code, message, token) {
56
+ var err = error(code, message, {
57
+ line: token.loc.start.line,
58
+ column: token.loc.start.column,
59
+ filename: this.filename,
60
+ src: this.src,
61
+ });
62
+ throw err;
63
+ },
64
+
65
+ /**
66
+ * Return the next token object.
67
+ *
68
+ * @return {Object}
69
+ * @api private
70
+ */
71
+
72
+ advance: function() {
73
+ return this.tokens.advance();
74
+ },
75
+
76
+ /**
77
+ * Single token lookahead.
78
+ *
79
+ * @return {Object}
80
+ * @api private
81
+ */
82
+
83
+ peek: function() {
84
+ return this.tokens.peek();
85
+ },
86
+
87
+ /**
88
+ * `n` token lookahead.
89
+ *
90
+ * @param {Number} n
91
+ * @return {Object}
92
+ * @api private
93
+ */
94
+
95
+ lookahead: function(n) {
96
+ return this.tokens.lookahead(n);
97
+ },
98
+
99
+ /**
100
+ * Parse input returning a string of js for evaluation.
101
+ *
102
+ * @return {String}
103
+ * @api public
104
+ */
105
+
106
+ parse: function() {
107
+ var block = this.emptyBlock(0);
108
+
109
+ while ('eos' != this.peek().type) {
110
+ if ('newline' == this.peek().type) {
111
+ this.advance();
112
+ } else if ('text-html' == this.peek().type) {
113
+ block.nodes = block.nodes.concat(this.parseTextHtml());
114
+ } else {
115
+ var expr = this.parseExpr();
116
+ if (expr) {
117
+ if (expr.type === 'Block') {
118
+ block.nodes = block.nodes.concat(expr.nodes);
119
+ } else {
120
+ block.nodes.push(expr);
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ return block;
127
+ },
128
+
129
+ /**
130
+ * Expect the given type, or throw an exception.
131
+ *
132
+ * @param {String} type
133
+ * @api private
134
+ */
135
+
136
+ expect: function(type) {
137
+ if (this.peek().type === type) {
138
+ return this.advance();
139
+ } else {
140
+ this.error(
141
+ 'INVALID_TOKEN',
142
+ 'expected "' + type + '", but got "' + this.peek().type + '"',
143
+ this.peek()
144
+ );
145
+ }
146
+ },
147
+
148
+ /**
149
+ * Accept the given `type`.
150
+ *
151
+ * @param {String} type
152
+ * @api private
153
+ */
154
+
155
+ accept: function(type) {
156
+ if (this.peek().type === type) {
157
+ return this.advance();
158
+ }
159
+ },
160
+
161
+ initBlock: function(line, nodes) {
162
+ /* istanbul ignore if */
163
+ if ((line | 0) !== line) throw new Error('`line` is not an integer');
164
+ /* istanbul ignore if */
165
+ if (!Array.isArray(nodes)) throw new Error('`nodes` is not an array');
166
+ return {
167
+ type: 'Block',
168
+ nodes: nodes,
169
+ line: line,
170
+ filename: this.filename,
171
+ };
172
+ },
173
+
174
+ emptyBlock: function(line) {
175
+ return this.initBlock(line, []);
176
+ },
177
+
178
+ runPlugin: function(context, tok) {
179
+ var rest = [this];
180
+ for (var i = 2; i < arguments.length; i++) {
181
+ rest.push(arguments[i]);
182
+ }
183
+ var pluginContext;
184
+ for (var i = 0; i < this.plugins.length; i++) {
185
+ var plugin = this.plugins[i];
186
+ if (plugin[context] && plugin[context][tok.type]) {
187
+ if (pluginContext)
188
+ throw new Error(
189
+ 'Multiple plugin handlers found for context ' +
190
+ JSON.stringify(context) +
191
+ ', token type ' +
192
+ JSON.stringify(tok.type)
193
+ );
194
+ pluginContext = plugin[context];
195
+ }
196
+ }
197
+ if (pluginContext)
198
+ return pluginContext[tok.type].apply(pluginContext, rest);
199
+ },
200
+
201
+ /**
202
+ * tag
203
+ * | doctype
204
+ * | mixin
205
+ * | include
206
+ * | filter
207
+ * | comment
208
+ * | text
209
+ * | text-html
210
+ * | dot
211
+ * | each
212
+ * | code
213
+ * | yield
214
+ * | id
215
+ * | class
216
+ * | interpolation
217
+ */
218
+
219
+ parseExpr: function() {
220
+ switch (this.peek().type) {
221
+ case 'tag':
222
+ return this.parseTag();
223
+ case 'mixin':
224
+ return this.parseMixin();
225
+ case 'block':
226
+ return this.parseBlock();
227
+ case 'mixin-block':
228
+ return this.parseMixinBlock();
229
+ case 'case':
230
+ return this.parseCase();
231
+ case 'extends':
232
+ return this.parseExtends();
233
+ case 'include':
234
+ return this.parseInclude();
235
+ case 'doctype':
236
+ return this.parseDoctype();
237
+ case 'filter':
238
+ return this.parseFilter();
239
+ case 'comment':
240
+ return this.parseComment();
241
+ case 'text':
242
+ case 'interpolated-code':
243
+ case 'start-pug-interpolation':
244
+ return this.parseText({block: true});
245
+ case 'text-html':
246
+ return this.initBlock(this.peek().loc.start.line, this.parseTextHtml());
247
+ case 'dot':
248
+ return this.parseDot();
249
+ case 'each':
250
+ return this.parseEach();
251
+ case 'eachOf':
252
+ return this.parseEachOf();
253
+ case 'code':
254
+ return this.parseCode();
255
+ case 'blockcode':
256
+ return this.parseBlockCode();
257
+ case 'if':
258
+ return this.parseConditional();
259
+ case 'while':
260
+ return this.parseWhile();
261
+ case 'call':
262
+ return this.parseCall();
263
+ case 'interpolation':
264
+ return this.parseInterpolation();
265
+ case 'yield':
266
+ return this.parseYield();
267
+ case 'id':
268
+ case 'class':
269
+ this.tokens.defer({
270
+ type: 'tag',
271
+ val: 'div',
272
+ loc: this.peek().loc,
273
+ filename: this.filename,
274
+ });
275
+ return this.parseExpr();
276
+ default:
277
+ var pluginResult = this.runPlugin('expressionTokens', this.peek());
278
+ if (pluginResult) return pluginResult;
279
+ this.error(
280
+ 'INVALID_TOKEN',
281
+ 'unexpected token "' + this.peek().type + '"',
282
+ this.peek()
283
+ );
284
+ }
285
+ },
286
+
287
+ parseDot: function() {
288
+ this.advance();
289
+ return this.parseTextBlock();
290
+ },
291
+
292
+ /**
293
+ * Text
294
+ */
295
+
296
+ parseText: function(options) {
297
+ var tags = [];
298
+ var lineno = this.peek().loc.start.line;
299
+ var nextTok = this.peek();
300
+ loop: while (true) {
301
+ switch (nextTok.type) {
302
+ case 'text':
303
+ var tok = this.advance();
304
+ tags.push({
305
+ type: 'Text',
306
+ val: tok.val,
307
+ line: tok.loc.start.line,
308
+ column: tok.loc.start.column,
309
+ filename: this.filename,
310
+ });
311
+ break;
312
+ case 'interpolated-code':
313
+ var tok = this.advance();
314
+ tags.push({
315
+ type: 'Code',
316
+ val: tok.val,
317
+ buffer: tok.buffer,
318
+ mustEscape: tok.mustEscape !== false,
319
+ isInline: true,
320
+ line: tok.loc.start.line,
321
+ column: tok.loc.start.column,
322
+ filename: this.filename,
323
+ });
324
+ break;
325
+ case 'newline':
326
+ if (!options || !options.block) break loop;
327
+ var tok = this.advance();
328
+ var nextType = this.peek().type;
329
+ if (nextType === 'text' || nextType === 'interpolated-code') {
330
+ tags.push({
331
+ type: 'Text',
332
+ val: '\n',
333
+ line: tok.loc.start.line,
334
+ column: tok.loc.start.column,
335
+ filename: this.filename,
336
+ });
337
+ }
338
+ break;
339
+ case 'start-pug-interpolation':
340
+ this.advance();
341
+ tags.push(this.parseExpr());
342
+ this.expect('end-pug-interpolation');
343
+ break;
344
+ default:
345
+ var pluginResult = this.runPlugin('textTokens', nextTok, tags);
346
+ if (pluginResult) break;
347
+ break loop;
348
+ }
349
+ nextTok = this.peek();
350
+ }
351
+ if (tags.length === 1) return tags[0];
352
+ else return this.initBlock(lineno, tags);
353
+ },
354
+
355
+ parseTextHtml: function() {
356
+ var nodes = [];
357
+ var currentNode = null;
358
+ loop: while (true) {
359
+ switch (this.peek().type) {
360
+ case 'text-html':
361
+ var text = this.advance();
362
+ if (!currentNode) {
363
+ currentNode = {
364
+ type: 'Text',
365
+ val: text.val,
366
+ filename: this.filename,
367
+ line: text.loc.start.line,
368
+ column: text.loc.start.column,
369
+ isHtml: true,
370
+ };
371
+ nodes.push(currentNode);
372
+ } else {
373
+ currentNode.val += '\n' + text.val;
374
+ }
375
+ break;
376
+ case 'indent':
377
+ var block = this.block();
378
+ block.nodes.forEach(function(node) {
379
+ if (node.isHtml) {
380
+ if (!currentNode) {
381
+ currentNode = node;
382
+ nodes.push(currentNode);
383
+ } else {
384
+ currentNode.val += '\n' + node.val;
385
+ }
386
+ } else {
387
+ currentNode = null;
388
+ nodes.push(node);
389
+ }
390
+ });
391
+ break;
392
+ case 'code':
393
+ currentNode = null;
394
+ nodes.push(this.parseCode(true));
395
+ break;
396
+ case 'newline':
397
+ this.advance();
398
+ break;
399
+ default:
400
+ break loop;
401
+ }
402
+ }
403
+ return nodes;
404
+ },
405
+
406
+ /**
407
+ * ':' expr
408
+ * | block
409
+ */
410
+
411
+ parseBlockExpansion: function() {
412
+ var tok = this.accept(':');
413
+ if (tok) {
414
+ var expr = this.parseExpr();
415
+ return expr.type === 'Block'
416
+ ? expr
417
+ : this.initBlock(tok.loc.start.line, [expr]);
418
+ } else {
419
+ return this.block();
420
+ }
421
+ },
422
+
423
+ /**
424
+ * case
425
+ */
426
+
427
+ parseCase: function() {
428
+ var tok = this.expect('case');
429
+ var node = {
430
+ type: 'Case',
431
+ expr: tok.val,
432
+ line: tok.loc.start.line,
433
+ column: tok.loc.start.column,
434
+ filename: this.filename,
435
+ };
436
+
437
+ var block = this.emptyBlock(tok.loc.start.line + 1);
438
+ this.expect('indent');
439
+ while ('outdent' != this.peek().type) {
440
+ switch (this.peek().type) {
441
+ case 'comment':
442
+ case 'newline':
443
+ this.advance();
444
+ break;
445
+ case 'when':
446
+ block.nodes.push(this.parseWhen());
447
+ break;
448
+ case 'default':
449
+ block.nodes.push(this.parseDefault());
450
+ break;
451
+ default:
452
+ var pluginResult = this.runPlugin('caseTokens', this.peek(), block);
453
+ if (pluginResult) break;
454
+ this.error(
455
+ 'INVALID_TOKEN',
456
+ 'Unexpected token "' +
457
+ this.peek().type +
458
+ '", expected "when", "default" or "newline"',
459
+ this.peek()
460
+ );
461
+ }
462
+ }
463
+ this.expect('outdent');
464
+
465
+ node.block = block;
466
+
467
+ return node;
468
+ },
469
+
470
+ /**
471
+ * when
472
+ */
473
+
474
+ parseWhen: function() {
475
+ var tok = this.expect('when');
476
+ if (this.peek().type !== 'newline') {
477
+ return {
478
+ type: 'When',
479
+ expr: tok.val,
480
+ block: this.parseBlockExpansion(),
481
+ debug: false,
482
+ line: tok.loc.start.line,
483
+ column: tok.loc.start.column,
484
+ filename: this.filename,
485
+ };
486
+ } else {
487
+ return {
488
+ type: 'When',
489
+ expr: tok.val,
490
+ debug: false,
491
+ line: tok.loc.start.line,
492
+ column: tok.loc.start.column,
493
+ filename: this.filename,
494
+ };
495
+ }
496
+ },
497
+
498
+ /**
499
+ * default
500
+ */
501
+
502
+ parseDefault: function() {
503
+ var tok = this.expect('default');
504
+ return {
505
+ type: 'When',
506
+ expr: 'default',
507
+ block: this.parseBlockExpansion(),
508
+ debug: false,
509
+ line: tok.loc.start.line,
510
+ column: tok.loc.start.column,
511
+ filename: this.filename,
512
+ };
513
+ },
514
+
515
+ /**
516
+ * code
517
+ */
518
+
519
+ parseCode: function(noBlock) {
520
+ var tok = this.expect('code');
521
+ assert(
522
+ typeof tok.mustEscape === 'boolean',
523
+ 'Please update to the newest version of pug-lexer.'
524
+ );
525
+ var node = {
526
+ type: 'Code',
527
+ val: tok.val,
528
+ buffer: tok.buffer,
529
+ mustEscape: tok.mustEscape !== false,
530
+ isInline: !!noBlock,
531
+ line: tok.loc.start.line,
532
+ column: tok.loc.start.column,
533
+ filename: this.filename,
534
+ };
535
+ // todo: why is this here? It seems like a hacky workaround
536
+ if (node.val.match(/^ *else/)) node.debug = false;
537
+
538
+ if (noBlock) return node;
539
+
540
+ var block;
541
+
542
+ // handle block
543
+ block = 'indent' == this.peek().type;
544
+ if (block) {
545
+ if (tok.buffer) {
546
+ this.error(
547
+ 'BLOCK_IN_BUFFERED_CODE',
548
+ 'Buffered code cannot have a block attached to it',
549
+ this.peek()
550
+ );
551
+ }
552
+ node.block = this.block();
553
+ }
554
+
555
+ return node;
556
+ },
557
+ parseConditional: function() {
558
+ var tok = this.expect('if');
559
+ var node = {
560
+ type: 'Conditional',
561
+ test: tok.val,
562
+ consequent: this.emptyBlock(tok.loc.start.line),
563
+ alternate: null,
564
+ line: tok.loc.start.line,
565
+ column: tok.loc.start.column,
566
+ filename: this.filename,
567
+ };
568
+
569
+ // handle block
570
+ if ('indent' == this.peek().type) {
571
+ node.consequent = this.block();
572
+ }
573
+
574
+ var currentNode = node;
575
+ while (true) {
576
+ if (this.peek().type === 'newline') {
577
+ this.expect('newline');
578
+ } else if (this.peek().type === 'else-if') {
579
+ tok = this.expect('else-if');
580
+ currentNode = currentNode.alternate = {
581
+ type: 'Conditional',
582
+ test: tok.val,
583
+ consequent: this.emptyBlock(tok.loc.start.line),
584
+ alternate: null,
585
+ line: tok.loc.start.line,
586
+ column: tok.loc.start.column,
587
+ filename: this.filename,
588
+ };
589
+ if ('indent' == this.peek().type) {
590
+ currentNode.consequent = this.block();
591
+ }
592
+ } else if (this.peek().type === 'else') {
593
+ this.expect('else');
594
+ if (this.peek().type === 'indent') {
595
+ currentNode.alternate = this.block();
596
+ }
597
+ break;
598
+ } else {
599
+ break;
600
+ }
601
+ }
602
+
603
+ return node;
604
+ },
605
+ parseWhile: function() {
606
+ var tok = this.expect('while');
607
+ var node = {
608
+ type: 'While',
609
+ test: tok.val,
610
+ line: tok.loc.start.line,
611
+ column: tok.loc.start.column,
612
+ filename: this.filename,
613
+ };
614
+
615
+ // handle block
616
+ if ('indent' == this.peek().type) {
617
+ node.block = this.block();
618
+ } else {
619
+ node.block = this.emptyBlock(tok.loc.start.line);
620
+ }
621
+
622
+ return node;
623
+ },
624
+
625
+ /**
626
+ * block code
627
+ */
628
+
629
+ parseBlockCode: function() {
630
+ var tok = this.expect('blockcode');
631
+ var line = tok.loc.start.line;
632
+ var column = tok.loc.start.column;
633
+ var body = this.peek();
634
+ var text = '';
635
+ if (body.type === 'start-pipeless-text') {
636
+ this.advance();
637
+ while (this.peek().type !== 'end-pipeless-text') {
638
+ tok = this.advance();
639
+ switch (tok.type) {
640
+ case 'text':
641
+ text += tok.val;
642
+ break;
643
+ case 'newline':
644
+ text += '\n';
645
+ break;
646
+ default:
647
+ var pluginResult = this.runPlugin('blockCodeTokens', tok, tok);
648
+ if (pluginResult) {
649
+ text += pluginResult;
650
+ break;
651
+ }
652
+ this.error(
653
+ 'INVALID_TOKEN',
654
+ 'Unexpected token type: ' + tok.type,
655
+ tok
656
+ );
657
+ }
658
+ }
659
+ this.advance();
660
+ }
661
+ return {
662
+ type: 'Code',
663
+ val: text,
664
+ buffer: false,
665
+ mustEscape: false,
666
+ isInline: false,
667
+ line: line,
668
+ column: column,
669
+ filename: this.filename,
670
+ };
671
+ },
672
+ /**
673
+ * comment
674
+ */
675
+
676
+ parseComment: function() {
677
+ var tok = this.expect('comment');
678
+ var block;
679
+ if ((block = this.parseTextBlock())) {
680
+ return {
681
+ type: 'BlockComment',
682
+ val: tok.val,
683
+ block: block,
684
+ buffer: tok.buffer,
685
+ line: tok.loc.start.line,
686
+ column: tok.loc.start.column,
687
+ filename: this.filename,
688
+ };
689
+ } else {
690
+ return {
691
+ type: 'Comment',
692
+ val: tok.val,
693
+ buffer: tok.buffer,
694
+ line: tok.loc.start.line,
695
+ column: tok.loc.start.column,
696
+ filename: this.filename,
697
+ };
698
+ }
699
+ },
700
+
701
+ /**
702
+ * doctype
703
+ */
704
+
705
+ parseDoctype: function() {
706
+ var tok = this.expect('doctype');
707
+ return {
708
+ type: 'Doctype',
709
+ val: tok.val,
710
+ line: tok.loc.start.line,
711
+ column: tok.loc.start.column,
712
+ filename: this.filename,
713
+ };
714
+ },
715
+
716
+ parseIncludeFilter: function() {
717
+ var tok = this.expect('filter');
718
+ var attrs = [];
719
+
720
+ if (this.peek().type === 'start-attributes') {
721
+ attrs = this.attrs();
722
+ }
723
+
724
+ return {
725
+ type: 'IncludeFilter',
726
+ name: tok.val,
727
+ attrs: attrs,
728
+ line: tok.loc.start.line,
729
+ column: tok.loc.start.column,
730
+ filename: this.filename,
731
+ };
732
+ },
733
+
734
+ /**
735
+ * filter attrs? text-block
736
+ */
737
+
738
+ parseFilter: function() {
739
+ var tok = this.expect('filter');
740
+ var block,
741
+ attrs = [];
742
+
743
+ if (this.peek().type === 'start-attributes') {
744
+ attrs = this.attrs();
745
+ }
746
+
747
+ if (this.peek().type === 'text') {
748
+ var textToken = this.advance();
749
+ block = this.initBlock(textToken.loc.start.line, [
750
+ {
751
+ type: 'Text',
752
+ val: textToken.val,
753
+ line: textToken.loc.start.line,
754
+ column: textToken.loc.start.column,
755
+ filename: this.filename,
756
+ },
757
+ ]);
758
+ } else if (this.peek().type === 'filter') {
759
+ block = this.initBlock(tok.loc.start.line, [this.parseFilter()]);
760
+ } else {
761
+ block = this.parseTextBlock() || this.emptyBlock(tok.loc.start.line);
762
+ }
763
+
764
+ return {
765
+ type: 'Filter',
766
+ name: tok.val,
767
+ block: block,
768
+ attrs: attrs,
769
+ line: tok.loc.start.line,
770
+ column: tok.loc.start.column,
771
+ filename: this.filename,
772
+ };
773
+ },
774
+
775
+ /**
776
+ * each block
777
+ */
778
+
779
+ parseEach: function() {
780
+ var tok = this.expect('each');
781
+ var node = {
782
+ type: 'Each',
783
+ obj: tok.code,
784
+ val: tok.val,
785
+ key: tok.key,
786
+ block: this.block(),
787
+ line: tok.loc.start.line,
788
+ column: tok.loc.start.column,
789
+ filename: this.filename,
790
+ };
791
+ if (this.peek().type == 'else') {
792
+ this.advance();
793
+ node.alternate = this.block();
794
+ }
795
+ return node;
796
+ },
797
+
798
+ parseEachOf: function() {
799
+ var tok = this.expect('eachOf');
800
+ var node = {
801
+ type: 'EachOf',
802
+ obj: tok.code,
803
+ val: tok.val,
804
+ block: this.block(),
805
+ line: tok.loc.start.line,
806
+ column: tok.loc.start.column,
807
+ filename: this.filename,
808
+ };
809
+ return node;
810
+ },
811
+ /**
812
+ * 'extends' name
813
+ */
814
+
815
+ parseExtends: function() {
816
+ var tok = this.expect('extends');
817
+ var path = this.expect('path');
818
+ return {
819
+ type: 'Extends',
820
+ file: {
821
+ type: 'FileReference',
822
+ path: path.val.trim(),
823
+ line: path.loc.start.line,
824
+ column: path.loc.start.column,
825
+ filename: this.filename,
826
+ },
827
+ line: tok.loc.start.line,
828
+ column: tok.loc.start.column,
829
+ filename: this.filename,
830
+ };
831
+ },
832
+
833
+ /**
834
+ * 'block' name block
835
+ */
836
+
837
+ parseBlock: function() {
838
+ var tok = this.expect('block');
839
+
840
+ var node =
841
+ 'indent' == this.peek().type
842
+ ? this.block()
843
+ : this.emptyBlock(tok.loc.start.line);
844
+ node.type = 'NamedBlock';
845
+ node.name = tok.val.trim();
846
+ node.mode = tok.mode;
847
+ node.line = tok.loc.start.line;
848
+ node.column = tok.loc.start.column;
849
+
850
+ return node;
851
+ },
852
+
853
+ parseMixinBlock: function() {
854
+ var tok = this.expect('mixin-block');
855
+ if (!this.inMixin) {
856
+ this.error(
857
+ 'BLOCK_OUTISDE_MIXIN',
858
+ 'Anonymous blocks are not allowed unless they are part of a mixin.',
859
+ tok
860
+ );
861
+ }
862
+ return {
863
+ type: 'MixinBlock',
864
+ line: tok.loc.start.line,
865
+ column: tok.loc.start.column,
866
+ filename: this.filename,
867
+ };
868
+ },
869
+
870
+ parseYield: function() {
871
+ var tok = this.expect('yield');
872
+ return {
873
+ type: 'YieldBlock',
874
+ line: tok.loc.start.line,
875
+ column: tok.loc.start.column,
876
+ filename: this.filename,
877
+ };
878
+ },
879
+
880
+ /**
881
+ * include block?
882
+ */
883
+
884
+ parseInclude: function() {
885
+ var tok = this.expect('include');
886
+ var node = {
887
+ type: 'Include',
888
+ file: {
889
+ type: 'FileReference',
890
+ filename: this.filename,
891
+ },
892
+ line: tok.loc.start.line,
893
+ column: tok.loc.start.column,
894
+ filename: this.filename,
895
+ };
896
+ var filters = [];
897
+ while (this.peek().type === 'filter') {
898
+ filters.push(this.parseIncludeFilter());
899
+ }
900
+ var path = this.expect('path');
901
+
902
+ node.file.path = path.val.trim();
903
+ node.file.line = path.loc.start.line;
904
+ node.file.column = path.loc.start.column;
905
+
906
+ if (
907
+ (/\.jade$/.test(node.file.path) || /\.pug$/.test(node.file.path)) &&
908
+ !filters.length
909
+ ) {
910
+ node.block =
911
+ 'indent' == this.peek().type
912
+ ? this.block()
913
+ : this.emptyBlock(tok.loc.start.line);
914
+ if (/\.jade$/.test(node.file.path)) {
915
+ console.warn(
916
+ this.filename +
917
+ ', line ' +
918
+ tok.loc.start.line +
919
+ ':\nThe .jade extension is deprecated, use .pug for "' +
920
+ node.file.path +
921
+ '".'
922
+ );
923
+ }
924
+ } else {
925
+ node.type = 'RawInclude';
926
+ node.filters = filters;
927
+ if (this.peek().type === 'indent') {
928
+ this.error(
929
+ 'RAW_INCLUDE_BLOCK',
930
+ 'Raw inclusion cannot contain a block',
931
+ this.peek()
932
+ );
933
+ }
934
+ }
935
+ return node;
936
+ },
937
+
938
+ /**
939
+ * call ident block
940
+ */
941
+
942
+ parseCall: function() {
943
+ var tok = this.expect('call');
944
+ var name = tok.val;
945
+ var args = tok.args;
946
+ var mixin = {
947
+ type: 'Mixin',
948
+ name: name,
949
+ args: args,
950
+ block: this.emptyBlock(tok.loc.start.line),
951
+ call: true,
952
+ attrs: [],
953
+ attributeBlocks: [],
954
+ line: tok.loc.start.line,
955
+ column: tok.loc.start.column,
956
+ filename: this.filename,
957
+ };
958
+
959
+ this.tag(mixin);
960
+ if (mixin.code) {
961
+ mixin.block.nodes.push(mixin.code);
962
+ delete mixin.code;
963
+ }
964
+ if (mixin.block.nodes.length === 0) mixin.block = null;
965
+ return mixin;
966
+ },
967
+
968
+ /**
969
+ * mixin block
970
+ */
971
+
972
+ parseMixin: function() {
973
+ var tok = this.expect('mixin');
974
+ var name = tok.val;
975
+ var args = tok.args;
976
+
977
+ if ('indent' == this.peek().type) {
978
+ this.inMixin++;
979
+ var mixin = {
980
+ type: 'Mixin',
981
+ name: name,
982
+ args: args,
983
+ block: this.block(),
984
+ call: false,
985
+ line: tok.loc.start.line,
986
+ column: tok.loc.start.column,
987
+ filename: this.filename,
988
+ };
989
+ this.inMixin--;
990
+ return mixin;
991
+ } else {
992
+ this.error(
993
+ 'MIXIN_WITHOUT_BODY',
994
+ 'Mixin ' + name + ' declared without body',
995
+ tok
996
+ );
997
+ }
998
+ },
999
+
1000
+ /**
1001
+ * indent (text | newline)* outdent
1002
+ */
1003
+
1004
+ parseTextBlock: function() {
1005
+ var tok = this.accept('start-pipeless-text');
1006
+ if (!tok) return;
1007
+ var block = this.emptyBlock(tok.loc.start.line);
1008
+ while (this.peek().type !== 'end-pipeless-text') {
1009
+ var tok = this.advance();
1010
+ switch (tok.type) {
1011
+ case 'text':
1012
+ block.nodes.push({
1013
+ type: 'Text',
1014
+ val: tok.val,
1015
+ line: tok.loc.start.line,
1016
+ column: tok.loc.start.column,
1017
+ filename: this.filename,
1018
+ });
1019
+ break;
1020
+ case 'newline':
1021
+ block.nodes.push({
1022
+ type: 'Text',
1023
+ val: '\n',
1024
+ line: tok.loc.start.line,
1025
+ column: tok.loc.start.column,
1026
+ filename: this.filename,
1027
+ });
1028
+ break;
1029
+ case 'start-pug-interpolation':
1030
+ block.nodes.push(this.parseExpr());
1031
+ this.expect('end-pug-interpolation');
1032
+ break;
1033
+ case 'interpolated-code':
1034
+ block.nodes.push({
1035
+ type: 'Code',
1036
+ val: tok.val,
1037
+ buffer: tok.buffer,
1038
+ mustEscape: tok.mustEscape !== false,
1039
+ isInline: true,
1040
+ line: tok.loc.start.line,
1041
+ column: tok.loc.start.column,
1042
+ filename: this.filename,
1043
+ });
1044
+ break;
1045
+ default:
1046
+ var pluginResult = this.runPlugin('textBlockTokens', tok, block, tok);
1047
+ if (pluginResult) break;
1048
+ this.error(
1049
+ 'INVALID_TOKEN',
1050
+ 'Unexpected token type: ' + tok.type,
1051
+ tok
1052
+ );
1053
+ }
1054
+ }
1055
+ this.advance();
1056
+ return block;
1057
+ },
1058
+
1059
+ /**
1060
+ * indent expr* outdent
1061
+ */
1062
+
1063
+ block: function() {
1064
+ var tok = this.expect('indent');
1065
+ var block = this.emptyBlock(tok.loc.start.line);
1066
+ while ('outdent' != this.peek().type) {
1067
+ if ('newline' == this.peek().type) {
1068
+ this.advance();
1069
+ } else if ('text-html' == this.peek().type) {
1070
+ block.nodes = block.nodes.concat(this.parseTextHtml());
1071
+ } else {
1072
+ var expr = this.parseExpr();
1073
+ if (expr.type === 'Block') {
1074
+ block.nodes = block.nodes.concat(expr.nodes);
1075
+ } else {
1076
+ block.nodes.push(expr);
1077
+ }
1078
+ }
1079
+ }
1080
+ this.expect('outdent');
1081
+ return block;
1082
+ },
1083
+
1084
+ /**
1085
+ * interpolation (attrs | class | id)* (text | code | ':')? newline* block?
1086
+ */
1087
+
1088
+ parseInterpolation: function() {
1089
+ var tok = this.advance();
1090
+ var tag = {
1091
+ type: 'InterpolatedTag',
1092
+ expr: tok.val,
1093
+ selfClosing: false,
1094
+ block: this.emptyBlock(tok.loc.start.line),
1095
+ attrs: [],
1096
+ attributeBlocks: [],
1097
+ isInline: false,
1098
+ line: tok.loc.start.line,
1099
+ column: tok.loc.start.column,
1100
+ filename: this.filename,
1101
+ };
1102
+
1103
+ return this.tag(tag, {selfClosingAllowed: true});
1104
+ },
1105
+
1106
+ /**
1107
+ * tag (attrs | class | id)* (text | code | ':')? newline* block?
1108
+ */
1109
+
1110
+ parseTag: function() {
1111
+ var tok = this.advance();
1112
+ var tag = {
1113
+ type: 'Tag',
1114
+ name: tok.val,
1115
+ selfClosing: false,
1116
+ block: this.emptyBlock(tok.loc.start.line),
1117
+ attrs: [],
1118
+ attributeBlocks: [],
1119
+ isInline: inlineTags.indexOf(tok.val) !== -1,
1120
+ line: tok.loc.start.line,
1121
+ column: tok.loc.start.column,
1122
+ filename: this.filename,
1123
+ };
1124
+
1125
+ return this.tag(tag, {selfClosingAllowed: true});
1126
+ },
1127
+
1128
+ /**
1129
+ * Parse tag.
1130
+ */
1131
+
1132
+ tag: function(tag, options) {
1133
+ var seenAttrs = false;
1134
+ var attributeNames = [];
1135
+ var selfClosingAllowed = options && options.selfClosingAllowed;
1136
+ // (attrs | class | id)*
1137
+ out: while (true) {
1138
+ switch (this.peek().type) {
1139
+ case 'id':
1140
+ case 'class':
1141
+ var tok = this.advance();
1142
+ if (tok.type === 'id') {
1143
+ if (attributeNames.indexOf('id') !== -1) {
1144
+ this.error(
1145
+ 'DUPLICATE_ID',
1146
+ 'Duplicate attribute "id" is not allowed.',
1147
+ tok
1148
+ );
1149
+ }
1150
+ attributeNames.push('id');
1151
+ }
1152
+ tag.attrs.push({
1153
+ name: tok.type,
1154
+ val: "'" + tok.val + "'",
1155
+ line: tok.loc.start.line,
1156
+ column: tok.loc.start.column,
1157
+ filename: this.filename,
1158
+ mustEscape: false,
1159
+ });
1160
+ continue;
1161
+ case 'start-attributes':
1162
+ if (seenAttrs) {
1163
+ console.warn(
1164
+ this.filename +
1165
+ ', line ' +
1166
+ this.peek().loc.start.line +
1167
+ ':\nYou should not have pug tags with multiple attributes.'
1168
+ );
1169
+ }
1170
+ seenAttrs = true;
1171
+ tag.attrs = tag.attrs.concat(this.attrs(attributeNames));
1172
+ continue;
1173
+ case '&attributes':
1174
+ var tok = this.advance();
1175
+ tag.attributeBlocks.push({
1176
+ type: 'AttributeBlock',
1177
+ val: tok.val,
1178
+ line: tok.loc.start.line,
1179
+ column: tok.loc.start.column,
1180
+ filename: this.filename,
1181
+ });
1182
+ break;
1183
+ default:
1184
+ var pluginResult = this.runPlugin(
1185
+ 'tagAttributeTokens',
1186
+ this.peek(),
1187
+ tag,
1188
+ attributeNames
1189
+ );
1190
+ if (pluginResult) break;
1191
+ break out;
1192
+ }
1193
+ }
1194
+
1195
+ // check immediate '.'
1196
+ if ('dot' == this.peek().type) {
1197
+ tag.textOnly = true;
1198
+ this.advance();
1199
+ }
1200
+
1201
+ // (text | code | ':')?
1202
+ switch (this.peek().type) {
1203
+ case 'text':
1204
+ case 'interpolated-code':
1205
+ var text = this.parseText();
1206
+ if (text.type === 'Block') {
1207
+ tag.block.nodes.push.apply(tag.block.nodes, text.nodes);
1208
+ } else {
1209
+ tag.block.nodes.push(text);
1210
+ }
1211
+ break;
1212
+ case 'code':
1213
+ tag.block.nodes.push(this.parseCode(true));
1214
+ break;
1215
+ case ':':
1216
+ this.advance();
1217
+ var expr = this.parseExpr();
1218
+ tag.block =
1219
+ expr.type === 'Block' ? expr : this.initBlock(tag.line, [expr]);
1220
+ break;
1221
+ case 'newline':
1222
+ case 'indent':
1223
+ case 'outdent':
1224
+ case 'eos':
1225
+ case 'start-pipeless-text':
1226
+ case 'end-pug-interpolation':
1227
+ break;
1228
+ case 'slash':
1229
+ if (selfClosingAllowed) {
1230
+ this.advance();
1231
+ tag.selfClosing = true;
1232
+ break;
1233
+ }
1234
+ default:
1235
+ var pluginResult = this.runPlugin(
1236
+ 'tagTokens',
1237
+ this.peek(),
1238
+ tag,
1239
+ options
1240
+ );
1241
+ if (pluginResult) break;
1242
+ this.error(
1243
+ 'INVALID_TOKEN',
1244
+ 'Unexpected token `' +
1245
+ this.peek().type +
1246
+ '` expected `text`, `interpolated-code`, `code`, `:`' +
1247
+ (selfClosingAllowed ? ', `slash`' : '') +
1248
+ ', `newline` or `eos`',
1249
+ this.peek()
1250
+ );
1251
+ }
1252
+
1253
+ // newline*
1254
+ while ('newline' == this.peek().type) this.advance();
1255
+
1256
+ // block?
1257
+ if (tag.textOnly) {
1258
+ tag.block = this.parseTextBlock() || this.emptyBlock(tag.line);
1259
+ } else if ('indent' == this.peek().type) {
1260
+ var block = this.block();
1261
+ for (var i = 0, len = block.nodes.length; i < len; ++i) {
1262
+ tag.block.nodes.push(block.nodes[i]);
1263
+ }
1264
+ }
1265
+
1266
+ return tag;
1267
+ },
1268
+
1269
+ attrs: function(attributeNames) {
1270
+ this.expect('start-attributes');
1271
+
1272
+ var attrs = [];
1273
+ var tok = this.advance();
1274
+ while (tok.type === 'attribute') {
1275
+ if (tok.name !== 'class' && attributeNames) {
1276
+ if (attributeNames.indexOf(tok.name) !== -1) {
1277
+ this.error(
1278
+ 'DUPLICATE_ATTRIBUTE',
1279
+ 'Duplicate attribute "' + tok.name + '" is not allowed.',
1280
+ tok
1281
+ );
1282
+ }
1283
+ attributeNames.push(tok.name);
1284
+ }
1285
+ attrs.push({
1286
+ name: tok.name,
1287
+ val: tok.val,
1288
+ line: tok.loc.start.line,
1289
+ column: tok.loc.start.column,
1290
+ filename: this.filename,
1291
+ mustEscape: tok.mustEscape !== false,
1292
+ });
1293
+ tok = this.advance();
1294
+ }
1295
+ this.tokens.defer(tok);
1296
+ this.expect('end-attributes');
1297
+ return attrs;
1298
+ },
1299
+ };