| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192 | 'use strict';var assert = require('assert');var TokenStream = require('token-stream');var error = require('pug-error');var inlineTags = require('./lib/inline-tags');module.exports = parse;module.exports.Parser = Parser;function parse(tokens, options) {  var parser = new Parser(tokens, options);  var ast = parser.parse();  return JSON.parse(JSON.stringify(ast));};/** * Initialize `Parser` with the given input `str` and `filename`. * * @param {String} str * @param {String} filename * @param {Object} options * @api public */function Parser(tokens, options) {  options = options || {};  if (!Array.isArray(tokens)) {    throw new Error('Expected tokens to be an Array but got "' + (typeof tokens) + '"');  }  if (typeof options !== 'object') {    throw new Error('Expected "options" to be an object but got "' + (typeof options) + '"');  }  this.tokens = new TokenStream(tokens);  this.filename = options.filename;  this.src = options.src;  this.inMixin = 0;  this.plugins = options.plugins || [];};/** * Parser prototype. */Parser.prototype = {  /**   * Save original constructor   */  constructor: Parser,  error: function (code, message, token) {    var err = error(code, message, {      line: token.loc.start.line,      column: token.loc.start.column,      filename: this.filename,      src: this.src    });    throw err;  },  /**   * Return the next token object.   *   * @return {Object}   * @api private   */  advance: function(){    return this.tokens.advance();  },  /**   * Single token lookahead.   *   * @return {Object}   * @api private   */  peek: function() {    return this.tokens.peek();  },  /**   * `n` token lookahead.   *   * @param {Number} n   * @return {Object}   * @api private   */  lookahead: function(n){    return this.tokens.lookahead(n);  },  /**   * Parse input returning a string of js for evaluation.   *   * @return {String}   * @api public   */  parse: function(){    var block = this.emptyBlock(0);    while ('eos' != this.peek().type) {      if ('newline' == this.peek().type) {        this.advance();      } else if ('text-html' == this.peek().type) {        block.nodes = block.nodes.concat(this.parseTextHtml());      } else {        var expr = this.parseExpr();        if (expr) {          if (expr.type === 'Block') {            block.nodes = block.nodes.concat(expr.nodes);          } else {            block.nodes.push(expr);          }        }      }    }    return block;  },  /**   * Expect the given type, or throw an exception.   *   * @param {String} type   * @api private   */  expect: function(type){    if (this.peek().type === type) {      return this.advance();    } else {      this.error('INVALID_TOKEN', 'expected "' + type + '", but got "' + this.peek().type + '"', this.peek());    }  },  /**   * Accept the given `type`.   *   * @param {String} type   * @api private   */  accept: function(type){    if (this.peek().type === type) {      return this.advance();    }  },  initBlock: function(line, nodes) {    /* istanbul ignore if */    if ((line | 0) !== line) throw new Error('`line` is not an integer');    /* istanbul ignore if */    if (!Array.isArray(nodes)) throw new Error('`nodes` is not an array');    return {      type: 'Block',      nodes: nodes,      line: line,      filename: this.filename    };  },  emptyBlock: function(line) {    return this.initBlock(line, []);  },  runPlugin: function(context, tok) {    var rest = [this];    for (var i = 2; i < arguments.length; i++) {      rest.push(arguments[i]);    }    var pluginContext;    for (var i = 0; i < this.plugins.length; i++) {      var plugin = this.plugins[i];      if (plugin[context] && plugin[context][tok.type]) {        if (pluginContext) throw new Error('Multiple plugin handlers found for context ' + JSON.stringify(context) + ', token type ' + JSON.stringify(tok.type));        pluginContext = plugin[context];      }    }    if (pluginContext) return pluginContext[tok.type].apply(pluginContext, rest);  },  /**   *   tag   * | doctype   * | mixin   * | include   * | filter   * | comment   * | text   * | text-html   * | dot   * | each   * | code   * | yield   * | id   * | class   * | interpolation   */  parseExpr: function(){    switch (this.peek().type) {      case 'tag':        return this.parseTag();      case 'mixin':        return this.parseMixin();      case 'block':        return this.parseBlock();      case 'mixin-block':        return this.parseMixinBlock();      case 'case':        return this.parseCase();      case 'extends':        return this.parseExtends();      case 'include':        return this.parseInclude();      case 'doctype':        return this.parseDoctype();      case 'filter':        return this.parseFilter();      case 'comment':        return this.parseComment();      case 'text':      case 'interpolated-code':      case 'start-pug-interpolation':        return this.parseText({block: true});      case 'text-html':        return this.initBlock(this.peek().loc.start.line, this.parseTextHtml());      case 'dot':        return this.parseDot();      case 'each':        return this.parseEach();      case 'code':        return this.parseCode();      case 'blockcode':        return this.parseBlockCode();      case 'if':        return this.parseConditional();      case 'while':        return this.parseWhile();      case 'call':        return this.parseCall();      case 'interpolation':        return this.parseInterpolation();      case 'yield':        return this.parseYield();      case 'id':      case 'class':        if (!this.peek().loc.start) debugger;        this.tokens.defer({          type: 'tag',          val: 'div',          loc: this.peek().loc,          filename: this.filename        });        return this.parseExpr();      default:        var pluginResult = this.runPlugin('expressionTokens', this.peek());        if (pluginResult) return pluginResult;        this.error('INVALID_TOKEN', 'unexpected token "' + this.peek().type + '"', this.peek());    }  },  parseDot: function() {    this.advance();    return this.parseTextBlock();  },  /**   * Text   */  parseText: function(options){    var tags = [];    var lineno = this.peek().loc.start.line;    var nextTok = this.peek();    loop:      while (true) {        switch (nextTok.type) {          case 'text':            var tok = this.advance();            tags.push({              type: 'Text',              val: tok.val,              line: tok.loc.start.line,              column: tok.loc.start.column,              filename: this.filename            });            break;          case 'interpolated-code':            var tok = this.advance();            tags.push({              type: 'Code',              val: tok.val,              buffer: tok.buffer,              mustEscape: tok.mustEscape !== false,              isInline: true,              line: tok.loc.start.line,              column: tok.loc.start.column,              filename: this.filename            });            break;          case 'newline':            if (!options || !options.block) break loop;            var tok = this.advance();            var nextType = this.peek().type;            if (nextType === 'text' || nextType === 'interpolated-code') {              tags.push({                type: 'Text',                val: '\n',                line: tok.loc.start.line,                column: tok.loc.start.column,                filename: this.filename              });            }            break;          case 'start-pug-interpolation':            this.advance();            tags.push(this.parseExpr());            this.expect('end-pug-interpolation');            break;          default:            var pluginResult = this.runPlugin('textTokens', nextTok, tags);            if (pluginResult) break;            break loop;        }        nextTok = this.peek();      }    if (tags.length === 1) return tags[0];    else return this.initBlock(lineno, tags);  },  parseTextHtml: function () {    var nodes = [];    var currentNode = null;loop:    while (true) {      switch (this.peek().type) {        case 'text-html':          var text = this.advance();          if (!currentNode) {            currentNode = {              type: 'Text',              val: text.val,              filename: this.filename,              line: text.loc.start.line,              column: text.loc.start.column,              isHtml: true            };            nodes.push(currentNode);          } else {            currentNode.val += '\n' + text.val;          }          break;        case 'indent':          var block = this.block();          block.nodes.forEach(function (node) {            if (node.isHtml) {              if (!currentNode) {                currentNode = node;                nodes.push(currentNode);              } else {                currentNode.val += '\n' + node.val;              }            } else {              currentNode = null;              nodes.push(node);            }          });          break;        case 'code':          currentNode = null;          nodes.push(this.parseCode(true));          break;        case 'newline':          this.advance();          break;        default:          break loop;      }    }    return nodes;  },  /**   *   ':' expr   * | block   */  parseBlockExpansion: function(){    var tok = this.accept(':');    if (tok) {      var expr = this.parseExpr();      return expr.type === 'Block' ? expr : this.initBlock(tok.loc.start.line, [expr]);    } else {      return this.block();    }  },  /**   * case   */  parseCase: function(){    var tok = this.expect('case');    var node = {      type: 'Case',      expr: tok.val,      line: tok.loc.start.line,      column: tok.loc.start.column,      filename: this.filename    };    var block = this.emptyBlock(tok.loc.start.line + 1);    this.expect('indent');    while ('outdent' != this.peek().type) {      switch (this.peek().type) {        case 'comment':        case 'newline':          this.advance();          break;        case 'when':          block.nodes.push(this.parseWhen());          break;        case 'default':          block.nodes.push(this.parseDefault());          break;        default:          var pluginResult = this.runPlugin('caseTokens', this.peek(), block);          if (pluginResult) break;          this.error('INVALID_TOKEN', 'Unexpected token "' + this.peek().type                          + '", expected "when", "default" or "newline"', this.peek());      }    }    this.expect('outdent');    node.block = block;    return node;  },  /**   * when   */  parseWhen: function(){    var tok = this.expect('when');    if (this.peek().type !== 'newline') {      return {        type: 'When',        expr: tok.val,        block: this.parseBlockExpansion(),        debug: false,        line: tok.loc.start.line,        column: tok.loc.start.column,        filename: this.filename      };    } else {      return {        type: 'When',        expr: tok.val,        debug: false,        line: tok.loc.start.line,        column: tok.loc.start.column,        filename: this.filename      };    }  },  /**   * default   */  parseDefault: function(){    var tok = this.expect('default');    return {      type: 'When',      expr: 'default',      block: this.parseBlockExpansion(),      debug: false,      line: tok.loc.start.line,      column: tok.loc.start.column,      filename: this.filename    };  },  /**   * code   */  parseCode: function(noBlock){    var tok = this.expect('code');    assert(typeof tok.mustEscape === 'boolean', 'Please update to the newest version of pug-lexer.');    var node = {      type: 'Code',      val: tok.val,      buffer: tok.buffer,      mustEscape: tok.mustEscape !== false,      isInline: !!noBlock,      line: tok.loc.start.line,      column: tok.loc.start.column,      filename: this.filename    };    // todo: why is this here?  It seems like a hacky workaround    if (node.val.match(/^ *else/)) node.debug = false;    if (noBlock) return node;    var block;    // handle block    block = 'indent' == this.peek().type;    if (block) {      if (tok.buffer) {        this.error('BLOCK_IN_BUFFERED_CODE', 'Buffered code cannot have a block attached to it', this.peek());      }      node.block = this.block();    }    return node;  },  parseConditional: function(){    var tok = this.expect('if');    var node = {      type: 'Conditional',      test: tok.val,      consequent: this.emptyBlock(tok.loc.start.line),      alternate: null,      line: tok.loc.start.line,      column: tok.loc.start.column,      filename: this.filename    };    // handle block    if ('indent' == this.peek().type) {      node.consequent = this.block();    }    var currentNode = node;    while (true) {      if (this.peek().type === 'newline') {        this.expect('newline');      } else if (this.peek().type === 'else-if') {        tok = this.expect('else-if');        currentNode = (          currentNode.alternate = {            type: 'Conditional',            test: tok.val,            consequent: this.emptyBlock(tok.loc.start.line),            alternate: null,            line: tok.loc.start.line,            column: tok.loc.start.column,            filename: this.filename          }        );        if ('indent' == this.peek().type) {          currentNode.consequent = this.block();        }      } else if (this.peek().type === 'else') {        this.expect('else');        if (this.peek().type === 'indent') {          currentNode.alternate = this.block();        }        break;      } else {        break;      }    }    return node;  },  parseWhile: function(){    var tok = this.expect('while');    var node = {      type: 'While',      test: tok.val,      line: tok.loc.start.line,      column: tok.loc.start.column,      filename: this.filename    };    // handle block    if ('indent' == this.peek().type) {      node.block = this.block();    } else {      node.block = this.emptyBlock(tok.loc.start.line);    }    return node;  },  /**   * block code   */  parseBlockCode: function(){    var tok = this.expect('blockcode');    var line = tok.loc.start.line;    var column = tok.loc.start.column;    var body = this.peek();    var text = '';    if (body.type === 'start-pipeless-text') {      this.advance();      while (this.peek().type !== 'end-pipeless-text') {        tok = this.advance();        switch (tok.type) {          case 'text':            text += tok.val;            break;          case 'newline':            text += '\n';            break;          default:            var pluginResult = this.runPlugin('blockCodeTokens', tok, tok);            if (pluginResult) {              text += pluginResult;              break;            }            this.error('INVALID_TOKEN', 'Unexpected token type: ' + tok.type, tok);        }      }      this.advance();    }    return {      type: 'Code',      val: text,      buffer: false,      mustEscape: false,      isInline: false,      line: line,      column: column,      filename: this.filename    };  },  /**   * comment   */  parseComment: function(){    var tok = this.expect('comment');    var block;    if (block = this.parseTextBlock()) {      return {        type: 'BlockComment',        val: tok.val,        block: block,        buffer: tok.buffer,        line: tok.loc.start.line,        column: tok.loc.start.column,        filename: this.filename      };    } else {      return {        type: 'Comment',        val: tok.val,        buffer: tok.buffer,        line: tok.loc.start.line,        column: tok.loc.start.column,        filename: this.filename      };    }  },  /**   * doctype   */  parseDoctype: function(){    var tok = this.expect('doctype');    return {      type: 'Doctype',      val: tok.val,      line: tok.loc.start.line,      column: tok.loc.start.column,      filename: this.filename    };  },  parseIncludeFilter: function() {    var tok = this.expect('filter');    var attrs = [];    if (this.peek().type === 'start-attributes') {      attrs = this.attrs();    }    return {      type: 'IncludeFilter',      name: tok.val,      attrs: attrs,      line: tok.loc.start.line,      column: tok.loc.start.column,      filename: this.filename    };  },  /**   * filter attrs? text-block   */  parseFilter: function(){    var tok = this.expect('filter');    var block, attrs = [];    if (this.peek().type === 'start-attributes') {      attrs = this.attrs();    }    if (this.peek().type === 'text') {      var textToken = this.advance();      block = this.initBlock(textToken.loc.start.line, [        {          type: 'Text',          val: textToken.val,          line: textToken.loc.start.line,          column: textToken.loc.start.column,          filename: this.filename        }      ]);    } else if (this.peek().type === 'filter') {      block = this.initBlock(tok.loc.start.line, [this.parseFilter()]);    } else {      block = this.parseTextBlock() || this.emptyBlock(tok.loc.start.line);    }    return {      type: 'Filter',      name: tok.val,      block: block,      attrs: attrs,      line: tok.loc.start.line,      column: tok.loc.start.column,      filename: this.filename    };  },  /**   * each block   */  parseEach: function(){    var tok = this.expect('each');    var node = {      type: 'Each',      obj: tok.code,      val: tok.val,      key: tok.key,      block: this.block(),      line: tok.loc.start.line,      column: tok.loc.start.column,      filename: this.filename    };    if (this.peek().type == 'else') {      this.advance();      node.alternate = this.block();    }    return node;  },  /**   * 'extends' name   */  parseExtends: function(){    var tok = this.expect('extends');    var path = this.expect('path');    return {      type: 'Extends',      file: {        type: 'FileReference',        path: path.val.trim(),        line: path.loc.start.line,        column: path.loc.start.column,        filename: this.filename      },      line: tok.loc.start.line,      column: tok.loc.start.column,      filename: this.filename    };  },  /**   * 'block' name block   */  parseBlock: function(){    var tok = this.expect('block');    var node = 'indent' == this.peek().type ? this.block() : this.emptyBlock(tok.loc.start.line);    node.type = 'NamedBlock';    node.name = tok.val.trim();    node.mode = tok.mode;    node.line = tok.loc.start.line;    node.column = tok.loc.start.column;    return node;  },  parseMixinBlock: function () {    var tok = this.expect('mixin-block');    if (!this.inMixin) {      this.error('BLOCK_OUTISDE_MIXIN', 'Anonymous blocks are not allowed unless they are part of a mixin.', tok);    }    return {      type: 'MixinBlock',      line: tok.loc.start.line,      column: tok.loc.start.column,      filename: this.filename    };  },  parseYield: function() {    var tok = this.expect('yield');    return {      type: 'YieldBlock',      line: tok.loc.start.line,      column: tok.loc.start.column,      filename: this.filename    };  },  /**   * include block?   */  parseInclude: function(){    var tok = this.expect('include');    var node = {      type: 'Include',      file: {        type: 'FileReference',        filename: this.filename      },      line: tok.loc.start.line,      column: tok.loc.start.column,      filename: this.filename    };    var filters = [];    while (this.peek().type === 'filter') {      filters.push(this.parseIncludeFilter());    }    var path = this.expect('path');    node.file.path = path.val.trim();    node.file.line = path.loc.start.line;    node.file.column = path.loc.start.column;    if ((/\.jade$/.test(node.file.path) || /\.pug$/.test(node.file.path)) && !filters.length) {      node.block = 'indent' == this.peek().type ? this.block() : this.emptyBlock(tok.loc.start.line);      if (/\.jade$/.test(node.file.path)) {        console.warn(          this.filename + ', line ' + tok.loc.start.line +          ':\nThe .jade extension is deprecated, use .pug for "' + node.file.path +'".'        );      }    } else {      node.type = 'RawInclude';      node.filters = filters;      if (this.peek().type === 'indent') {        this.error('RAW_INCLUDE_BLOCK', 'Raw inclusion cannot contain a block', this.peek());      }    }    return node;  },  /**   * call ident block   */  parseCall: function(){    var tok = this.expect('call');    var name = tok.val;    var args = tok.args;    var mixin = {      type: 'Mixin',      name: name,      args: args,      block: this.emptyBlock(tok.loc.start.line),      call: true,      attrs: [],      attributeBlocks: [],      line: tok.loc.start.line,      column: tok.loc.start.column,      filename: this.filename    };    this.tag(mixin);    if (mixin.code) {      mixin.block.nodes.push(mixin.code);      delete mixin.code;    }    if (mixin.block.nodes.length === 0) mixin.block = null;    return mixin;  },  /**   * mixin block   */  parseMixin: function(){    var tok = this.expect('mixin');    var name = tok.val;    var args = tok.args;    if ('indent' == this.peek().type) {      this.inMixin++;      var mixin = {        type: 'Mixin',        name: name,        args: args,        block: this.block(),        call: false,        line: tok.loc.start.line,        column: tok.loc.start.column,        filename: this.filename      };      this.inMixin--;      return mixin;    } else {      this.error('MIXIN_WITHOUT_BODY', 'Mixin ' + name + ' declared without body', tok);    }  },  /**   * indent (text | newline)* outdent   */  parseTextBlock: function(){    var tok = this.accept('start-pipeless-text');    if (!tok) return;    var block = this.emptyBlock(tok.loc.start.line);    while (this.peek().type !== 'end-pipeless-text') {      var tok = this.advance();      switch (tok.type) {        case 'text':          block.nodes.push({            type: 'Text',            val: tok.val,            line: tok.loc.start.line,            column: tok.loc.start.column,            filename: this.filename          });          break;        case 'newline':          block.nodes.push({            type: 'Text',            val: '\n',            line: tok.loc.start.line,            column: tok.loc.start.column,            filename: this.filename          });          break;        case 'start-pug-interpolation':          block.nodes.push(this.parseExpr());          this.expect('end-pug-interpolation');          break;        case 'interpolated-code':          block.nodes.push({            type: 'Code',            val: tok.val,            buffer: tok.buffer,            mustEscape: tok.mustEscape !== false,            isInline: true,            line: tok.loc.start.line,            column: tok.loc.start.column,            filename: this.filename          });          break;        default:          var pluginResult = this.runPlugin('textBlockTokens', tok, block, tok);          if (pluginResult) break;          this.error('INVALID_TOKEN', 'Unexpected token type: ' + tok.type, tok);      }    }    this.advance();    return block;  },  /**   * indent expr* outdent   */  block: function(){    var tok = this.expect('indent');    var block = this.emptyBlock(tok.loc.start.line);    while ('outdent' != this.peek().type) {      if ('newline' == this.peek().type) {        this.advance();      } else if ('text-html' == this.peek().type) {        block.nodes = block.nodes.concat(this.parseTextHtml());      } else {        var expr = this.parseExpr();        if (expr.type === 'Block') {          block.nodes = block.nodes.concat(expr.nodes);        } else {          block.nodes.push(expr);        }      }    }    this.expect('outdent');    return block;  },  /**   * interpolation (attrs | class | id)* (text | code | ':')? newline* block?   */  parseInterpolation: function(){    var tok = this.advance();    var tag = {      type: 'InterpolatedTag',      expr: tok.val,      selfClosing: false,      block: this.emptyBlock(tok.loc.start.line),      attrs: [],      attributeBlocks: [],      isInline: false,      line: tok.loc.start.line,      column: tok.loc.start.column,      filename: this.filename    };    return this.tag(tag, {selfClosingAllowed: true});  },  /**   * tag (attrs | class | id)* (text | code | ':')? newline* block?   */  parseTag: function(){    var tok = this.advance();    var tag = {      type: 'Tag',      name: tok.val,      selfClosing: false,      block: this.emptyBlock(tok.loc.start.line),      attrs: [],      attributeBlocks: [],      isInline: inlineTags.indexOf(tok.val) !== -1,      line: tok.loc.start.line,      column: tok.loc.start.column,      filename: this.filename    };    return this.tag(tag, {selfClosingAllowed: true});  },  /**   * Parse tag.   */  tag: function(tag, options) {    var seenAttrs = false;    var attributeNames = [];    var selfClosingAllowed = options && options.selfClosingAllowed;    // (attrs | class | id)*    out:      while (true) {        switch (this.peek().type) {          case 'id':          case 'class':            var tok = this.advance();            if (tok.type === 'id') {              if (attributeNames.indexOf('id') !== -1) {                this.error('DUPLICATE_ID', 'Duplicate attribute "id" is not allowed.', tok);              }              attributeNames.push('id');            }            tag.attrs.push({              name: tok.type,              val: "'" + tok.val + "'",              line: tok.loc.start.line,              column: tok.loc.start.column,              filename: this.filename,              mustEscape: false            });            continue;          case 'start-attributes':            if (seenAttrs) {              console.warn(this.filename + ', line ' + this.peek().loc.start.line + ':\nYou should not have pug tags with multiple attributes.');            }            seenAttrs = true;            tag.attrs = tag.attrs.concat(this.attrs(attributeNames));            continue;          case '&attributes':            var tok = this.advance();            tag.attributeBlocks.push({              type: 'AttributeBlock',              val: tok.val,              line: tok.loc.start.line,              column: tok.loc.start.column,              filename: this.filename            });            break;          default:            var pluginResult = this.runPlugin('tagAttributeTokens', this.peek(), tag, attributeNames);            if (pluginResult) break;            break out;        }      }    // check immediate '.'    if ('dot' == this.peek().type) {      tag.textOnly = true;      this.advance();    }    // (text | code | ':')?    switch (this.peek().type) {      case 'text':      case 'interpolated-code':        var text = this.parseText();        if (text.type === 'Block') {          tag.block.nodes.push.apply(tag.block.nodes, text.nodes);        } else {          tag.block.nodes.push(text);        }        break;      case 'code':        tag.block.nodes.push(this.parseCode(true));        break;      case ':':        this.advance();        var expr = this.parseExpr();        tag.block = expr.type === 'Block' ? expr : this.initBlock(tag.line, [expr]);        break;      case 'newline':      case 'indent':      case 'outdent':      case 'eos':      case 'start-pipeless-text':      case 'end-pug-interpolation':        break;      case 'slash':        if (selfClosingAllowed) {          this.advance();          tag.selfClosing = true;          break;        }      default:        var pluginResult = this.runPlugin('tagTokens', this.peek(), tag, options);        if (pluginResult) break;        this.error('INVALID_TOKEN', 'Unexpected token `' + this.peek().type + '` expected `text`, `interpolated-code`, `code`, `:`' + (selfClosingAllowed ? ', `slash`' : '') + ', `newline` or `eos`', this.peek())    }    // newline*    while ('newline' == this.peek().type) this.advance();    // block?    if (tag.textOnly) {      tag.block = this.parseTextBlock() || this.emptyBlock(tag.line);    } else if ('indent' == this.peek().type) {      var block = this.block();      for (var i = 0, len = block.nodes.length; i < len; ++i) {        tag.block.nodes.push(block.nodes[i]);      }    }    return tag;  },  attrs: function(attributeNames) {    this.expect('start-attributes');    var attrs = [];    var tok = this.advance();    while (tok.type === 'attribute') {      if (tok.name !== 'class' && attributeNames) {        if (attributeNames.indexOf(tok.name) !== -1) {          this.error('DUPLICATE_ATTRIBUTE', 'Duplicate attribute "' + tok.name + '" is not allowed.', tok);        }        attributeNames.push(tok.name);      }      attrs.push({        name: tok.name,        val: tok.val,        line: tok.loc.start.line,        column: tok.loc.start.column,        filename: this.filename,        mustEscape: tok.mustEscape !== false      });      tok = this.advance();    }    this.tokens.defer(tok);    this.expect('end-attributes');    return attrs;  }};
 |