docs: 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;
    }