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