<commit_before>
/*!
 * Jade - Parser
 * Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
 * MIT Licensed
 */

/**
 * Module dependencies.
 */

var Lexer = require('./lexer')
  , nodes = require('./nodes')
  , utils = require('./utils')
  , filters = require('./filters')
  , path = require('./node-path')
  , extname = path.extname;

/**
 * Initialize `Parser` with the given input `str` and `filename`.
 *
 * @param {String} str
 * @param {String} filename
 * @param {Object} options
 * @api public
 */

var Parser = exports = module.exports = function Parser(str, filename, options){
  this.input = str;
  this.lexer = new Lexer(str, options);
  this.filename = filename;
  this.blocks = {};
  this.mixins = {};
  this.options = options;
  this.contexts = [this];
};

/**
 * Tags that may not contain tags.
 */

var textOnly = exports.textOnly = ['script', 'style'];

/**
 * Parser prototype.
 */

Parser.prototype = {

  /**
   * Push `parser` onto the context stack,
   * or pop and return a `Parser`.
   */

  context: function(parser){
    if (parser) {
      this.contexts.push(parser);
    } else {
      return this.contexts.pop();
    }
  },

  /**
   * Return the next token object.
   *
   * @return {Object}
   * @api private
   */

  advance: function(){
    return this.lexer.advance();
  },

  /**
   * Skip `n` tokens.
   *
   * @param {Number} n
   * @api private
   */

  skip: function(n){
    while (n--) this.advance();
  },

  /**
   * Single token lookahead.
   *
   * @return {Object}
   * @api private
   */

  peek: function() {
    return this.lookahead(1);
  },

  /**
   * Return lexer lineno.
   *
   * @return {Number}
   * @api private
   */

  line: function() {
    return this.lexer.lineno;
  },

  /**
   * `n` token lookahead.
   *
   * @param {Number} n
   * @return {Object}
   * @api private
   */

  lookahead: function(n){
    return this.lexer.lookahead(n);
  },

  /**
   * Parse input returning a string of js for evaluation.
   *
   * @return {String}
   * @api public
   */

  parse: function(){
    var block = new nodes.Block, parser;
    block.line = this.line();

    while ('eos' != this.peek().type) {
      if ('newline' == this.peek().type) {
        this.advance();
      } else {
        block.push(this.parseExpr());
      }
    }

    if (parser = this.extending) {
      this.context(parser);
      var ast = parser.parse();
      this.context();
      // hoist mixins
      for (var name in this.mixins)
        ast.unshift(this.mixins[name]);
      return ast;
    }

    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 {
      throw new Error('expected "' + type + '", but got "' + this.peek().type + '"');
    }
  },

  /**
   * Accept the given `type`.
   *
   * @param {String} type
   * @api private
   */

  accept: function(type){
    if (this.peek().type === type) {
      return this.advance();
    }
  },

  /**
   *   tag
   * | doctype
   * | mixin
   * | include
   * | filter
   * | comment
   * | text
   * | 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 'case':
        return this.parseCase();
      case 'when':
        return this.parseWhen();
      case 'default':
        return this.parseDefault();
      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':
        return this.parseText();
      case 'each':
        return this.parseEach();
      case 'code':
        return this.parseCode();
      case 'call':
        return this.parseCall();
      case 'interpolation':
        return this.parseInterpolation();
      case 'yield':
        this.advance();
        var block = new nodes.Block;
        block.yield = true;
        return block;
      case 'id':
      case 'class':
        var tok = this.advance();
        this.lexer.defer(this.lexer.tok('tag', 'div'));
        this.lexer.defer(tok);
        return this.parseExpr();
      default:
        throw new Error('unexpected token "' + this.peek().type + '"');
    }
  },

  /**
   * Text
   */

  parseText: function(){
    var tok = this.expect('text')
      , node = new nodes.Text(tok.val);
    node.line = this.line();
    return node;
  },

  /**
   *   ':' expr
   * | block
   */

  parseBlockExpansion: function(){
    if (':' == this.peek().type) {
      this.advance();
      return new nodes.Block(this.parseExpr());
    } else {
      return this.block();
    }
  },

  /**
   * case
   */

  parseCase: function(){
    var val = this.expect('case').val
      , node = new nodes.Case(val);
    node.line = this.line();
    node.block = this.block();
    return node;
  },

  /**
   * when
   */

  parseWhen: function(){
    var val = this.expect('when').val
    return new nodes.Case.When(val, this.parseBlockExpansion());
  },

  /**
   * default
   */

  parseDefault: function(){
    this.expect('default');
    return new nodes.Case.When('default', this.parseBlockExpansion());
  },

  /**
   * code
   */

  parseCode: function(){
    var tok = this.expect('code')
      , node = new nodes.Code(tok.val, tok.buffer, tok.escape)
      , block
      , i = 1;
    node.line = this.line();
    while (this.lookahead(i) && 'newline' == this.lookahead(i).type) ++i;
    block = 'indent' == this.lookahead(i).type;
    if (block) {
      this.skip(i-1);
      node.block = this.block();
    }
    return node;
  },

  /**
   * comment
   */

  parseComment: function(){
    var tok = this.expect('comment')
      , node;

    if ('indent' == this.peek().type) {
      node = new nodes.BlockComment(tok.val, this.block(), tok.buffer);
    } else {
      node = new nodes.Comment(tok.val, tok.buffer);
    }

    node.line = this.line();
    return node;
  },

  /**
   * doctype
   */

  parseDoctype: function(){
    var tok = this.expect('doctype')
      , node = new nodes.Doctype(tok.val);
    node.line = this.line();
    return node;
  },

  /**
   * filter attrs? text-block
   */

  parseFilter: function(){
    var block
      , tok = this.expect('filter')
      , attrs = this.accept('attrs');

    this.lexer.pipeless = true;
    block = this.parseTextBlock();
    this.lexer.pipeless = false;

    var node = new nodes.Filter(tok.val, block, attrs && attrs.attrs);
    node.line = this.line();
    return node;
  },

  /**
   * each block
   */

  parseEach: function(){
    var tok = this.expect('each')
      , node = new nodes.Each(tok.code, tok.val, tok.key);
    node.line = this.line();
    node.block = this.block();
    if (this.peek().type == 'code' && this.peek().val == 'else') {
      this.advance();
      node.alternative = this.block();
    }
    return node;
  },

  /**
   * 'extends' name
   */

  parseExtends: function(){
    var path = require('path')
      , fs = require('fs')
      , dirname = path.dirname
      , basename = path.basename
      , join = path.join;

    if (!this.filename)
      throw new Error('the "filename" option is required to extend templates');

    var path = this.expect('extends').val.trim()
      , dir = dirname(this.filename);

    var path = join(dir, path + '.jade')
      , str = fs.readFileSync(path, 'utf8')
      , parser = new Parser(str, path, this.options);

    parser.blocks = this.blocks;
    parser.contexts = this.contexts;
    this.extending = parser;

    // TODO: null node
    return new nodes.Literal('');
  },

  /**
   * 'block' name block
   */

  parseBlock: function(){
    var block = this.expect('block')
      , mode = block.mode
      , name = block.val.trim();

    block = 'indent' == this.peek().type
      ? this.block()
      : new nodes.Block(new nodes.Literal(''));

    var prev = this.blocks[name];

    if (prev) {
      switch (prev.mode) {
        case 'append':
          block.nodes = block.nodes.concat(prev.nodes);
          prev = block;
          break;
        case 'prepend':
          block.nodes = prev.nodes.concat(block.nodes);
          prev = block;
          break;
      }
    }

    block.mode = mode;
    return this.blocks[name] = prev || block;
  },

  /**
   * include block?
   */

  parseInclude: function(){
    var path = require('path')
      , fs = require('fs')
      , dirname = path.dirname
      , basename = path.basename
      , join = path.join;

    var path = this.expect('include').val.trim()
      , dir = dirname(this.filename);

    if (!this.filename)
      throw new Error('the "filename" option is required to use includes');

    // no extension
    if (!~basename(path).indexOf('.')) {
      path += '.jade';
    }

    // non-jade
    if ('.jade' != path.substr(-5)) {
      var path = join(dir, path);
      var str = fs.readFileSync(path, 'utf8');
      var ext = extname(path).slice(1);
      var filter = filters[ext];
      if (filter) str = filter(str);
      return new nodes.Literal(str);
    }

    var path = join(dir, path);
    var str = fs.readFileSync(path, 'utf8');
    var parser = new Parser(str, path, this.options);
    parser.blocks = utils.merge({}, this.blocks);
    parser.mixins = this.mixins;

    this.context(parser);
    var ast = parser.parse();
    this.context();
    ast.filename = path;

    if ('indent' == this.peek().type) {
      ast.includeBlock().push(this.block());
    }

    return ast;
  },

  /**
   * call ident block
   */

  parseCall: function(){
    var tok = this.expect('call')
      , name = tok.val
      , args = tok.args
      , mixin = new nodes.Mixin(name, args, new nodes.Block, true);

    this.tag(mixin);
    if (mixin.block.isEmpty()) mixin.block = null;
    return mixin;
  },

  /**
   * mixin block
   */

  parseMixin: function(){
    var tok = this.expect('mixin')
      , name = tok.val
      , args = tok.args
      , mixin;

    // definition
    if ('indent' == this.peek().type) {
      mixin = new nodes.Mixin(name, args, this.block(), false);
      this.mixins[name] = mixin;
      return mixin;
    // call
    } else {
      return new nodes.Mixin(name, args, null, true);
    }
  },

  /**
   * indent (text | newline)* outdent
   */

  parseTextBlock: function(){
    var block = new nodes.Block;
    block.line = this.line();
    var spaces = this.expect('indent').val;
    if (null == this._spaces) this._spaces = spaces;
    var indent = Array(spaces - this._spaces + 1).join(' ');
    while ('outdent' != this.peek().type) {
      switch (this.peek().type) {
        case 'newline':
          this.advance();
          break;
        case 'indent':
          this.parseTextBlock().nodes.forEach(function(node){
            block.push(node);
          });
          break;
        default:
          var text = new nodes.Text(indent + this.advance().val);
          text.line = this.line();
          block.push(text);
      }
    }

    if (spaces == this._spaces) this._spaces = null;
    this.expect('outdent');
    return block;
  },

  /**
   * indent expr* outdent
   */

  block: function(){
    var block = new nodes.Block;
    block.line = this.line();
    this.expect('indent');
    while ('outdent' != this.peek().type) {
      if ('newline' == this.peek().type) {
        this.advance();
      } else {
        block.push(this.parseExpr());
      }
    }
    this.expect('outdent');
    return block;
  },

  /**
   * interpolation (attrs | class | id)* (text | code | ':')? newline* block?
   */

  parseInterpolation: function(){
    var tok = this.advance();
    var tag = new nodes.Tag(tok.val);
    tag.buffer = true;
    return this.tag(tag);
  },

  /**
   * tag (attrs | class | id)* (text | code | ':')? newline* block?
   */

  parseTag: function(){
    // ast-filter look-ahead
    var i = 2;
    if ('attrs' == this.lookahead(i).type) ++i;

    var tok = this.advance()
      , tag = new nodes.Tag(tok.val);

    tag.selfClosing = tok.selfClosing;

    return this.tag(tag);
  },

  /**
   * Parse tag.
   */

  tag: function(tag){
    var dot;

    tag.line = this.line();

    // (attrs | class | id)*
    out:
      while (true) {
        switch (this.peek().type) {
          case 'id':
          case 'class':
            var tok = this.advance();
            tag.setAttribute(tok.type, "'" + tok.val + "'");
            continue;
          case 'attrs':
            var tok = this.advance()
              , obj = tok.attrs
              , escaped = tok.escaped
              , names = Object.keys(obj);

            if (tok.selfClosing) tag.selfClosing = true;

            for (var i = 0, len = names.length; i < len; ++i) {
              var name = names[i]
                , val = obj[name];
              tag.setAttribute(name, val, escaped[name]);
            }
            continue;
          default:
            break out;
        }
      }

    // check immediate '.'
    if ('.' == this.peek().val) {
      dot = tag.textOnly = true;
      this.advance();
    }

    // (text | code | ':')?
    switch (this.peek().type) {
      case 'text':
        tag.block.push(this.parseText());
        break;
      case 'code':
        tag.code = this.parseCode();
        break;
      case ':':
        this.advance();
        tag.block = new nodes.Block;
        tag.block.push(this.parseExpr());
        break;
    }

    // newline*
    while ('newline' == this.peek().type) this.advance();

    tag.textOnly = tag.textOnly || ~textOnly.indexOf(tag.name);

    // script special-case
    if ('script' == tag.name) {
      var type = tag.getAttribute('type');
      if (!dot && type && 'text/javascript' != type.replace(/^['"]|['"]$/g, '')) {
        tag.textOnly = false;
      }
    }

    // block?
    if ('indent' == this.peek().type) {
      if (tag.textOnly) {
        this.lexer.pipeless = true;
        tag.block = this.parseTextBlock();
        this.lexer.pipeless = false;
      } else {
        var block = this.block();
        if (tag.block) {
          for (var i = 0, len = block.nodes.length; i < len; ++i) {
            tag.block.push(block.nodes[i]);
          }
        } else {
          tag.block = block;
        }
      }
    }

    return tag;
  }
};
<commit_msg>add support for the .jadec extension<commit_after>
/*!
 * Jade - Parser
 * Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
 * MIT Licensed
 */

/**
 * Module dependencies.
 */

var Lexer = require('./lexer')
  , nodes = require('./nodes')
  , utils = require('./utils')
  , filters = require('./filters')
  , path = require('./node-path')
  , extname = path.extname;

/**
 * Initialize `Parser` with the given input `str` and `filename`.
 *
 * @param {String} str
 * @param {String} filename
 * @param {Object} options
 * @api public
 */

var Parser = exports = module.exports = function Parser(str, filename, options){
  this.input = str;
  this.lexer = new Lexer(str, options);
  this.filename = filename;
  this.blocks = {};
  this.mixins = {};
  this.options = options;
  this.contexts = [this];
};

/**
 * Tags that may not contain tags.
 */

var textOnly = exports.textOnly = ['script', 'style'];

/**
 * Parser prototype.
 */

Parser.prototype = {

  /**
   * Push `parser` onto the context stack,
   * or pop and return a `Parser`.
   */

  context: function(parser){
    if (parser) {
      this.contexts.push(parser);
    } else {
      return this.contexts.pop();
    }
  },

  /**
   * Return the next token object.
   *
   * @return {Object}
   * @api private
   */

  advance: function(){
    return this.lexer.advance();
  },

  /**
   * Skip `n` tokens.
   *
   * @param {Number} n
   * @api private
   */

  skip: function(n){
    while (n--) this.advance();
  },

  /**
   * Single token lookahead.
   *
   * @return {Object}
   * @api private
   */

  peek: function() {
    return this.lookahead(1);
  },

  /**
   * Return lexer lineno.
   *
   * @return {Number}
   * @api private
   */

  line: function() {
    return this.lexer.lineno;
  },

  /**
   * `n` token lookahead.
   *
   * @param {Number} n
   * @return {Object}
   * @api private
   */

  lookahead: function(n){
    return this.lexer.lookahead(n);
  },

  /**
   * Parse input returning a string of js for evaluation.
   *
   * @return {String}
   * @api public
   */

  parse: function(){
    var block = new nodes.Block, parser;
    block.line = this.line();

    while ('eos' != this.peek().type) {
      if ('newline' == this.peek().type) {
        this.advance();
      } else {
        block.push(this.parseExpr());
      }
    }

    if (parser = this.extending) {
      this.context(parser);
      var ast = parser.parse();
      this.context();
      // hoist mixins
      for (var name in this.mixins)
        ast.unshift(this.mixins[name]);
      return ast;
    }

    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 {
      throw new Error('expected "' + type + '", but got "' + this.peek().type + '"');
    }
  },

  /**
   * Accept the given `type`.
   *
   * @param {String} type
   * @api private
   */

  accept: function(type){
    if (this.peek().type === type) {
      return this.advance();
    }
  },

  /**
   *   tag
   * | doctype
   * | mixin
   * | include
   * | filter
   * | comment
   * | text
   * | 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 'case':
        return this.parseCase();
      case 'when':
        return this.parseWhen();
      case 'default':
        return this.parseDefault();
      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':
        return this.parseText();
      case 'each':
        return this.parseEach();
      case 'code':
        return this.parseCode();
      case 'call':
        return this.parseCall();
      case 'interpolation':
        return this.parseInterpolation();
      case 'yield':
        this.advance();
        var block = new nodes.Block;
        block.yield = true;
        return block;
      case 'id':
      case 'class':
        var tok = this.advance();
        this.lexer.defer(this.lexer.tok('tag', 'div'));
        this.lexer.defer(tok);
        return this.parseExpr();
      default:
        throw new Error('unexpected token "' + this.peek().type + '"');
    }
  },

  /**
   * Text
   */

  parseText: function(){
    var tok = this.expect('text')
      , node = new nodes.Text(tok.val);
    node.line = this.line();
    return node;
  },

  /**
   *   ':' expr
   * | block
   */

  parseBlockExpansion: function(){
    if (':' == this.peek().type) {
      this.advance();
      return new nodes.Block(this.parseExpr());
    } else {
      return this.block();
    }
  },

  /**
   * case
   */

  parseCase: function(){
    var val = this.expect('case').val
      , node = new nodes.Case(val);
    node.line = this.line();
    node.block = this.block();
    return node;
  },

  /**
   * when
   */

  parseWhen: function(){
    var val = this.expect('when').val
    return new nodes.Case.When(val, this.parseBlockExpansion());
  },

  /**
   * default
   */

  parseDefault: function(){
    this.expect('default');
    return new nodes.Case.When('default', this.parseBlockExpansion());
  },

  /**
   * code
   */

  parseCode: function(){
    var tok = this.expect('code')
      , node = new nodes.Code(tok.val, tok.buffer, tok.escape)
      , block
      , i = 1;
    node.line = this.line();
    while (this.lookahead(i) && 'newline' == this.lookahead(i).type) ++i;
    block = 'indent' == this.lookahead(i).type;
    if (block) {
      this.skip(i-1);
      node.block = this.block();
    }
    return node;
  },

  /**
   * comment
   */

  parseComment: function(){
    var tok = this.expect('comment')
      , node;

    if ('indent' == this.peek().type) {
      node = new nodes.BlockComment(tok.val, this.block(), tok.buffer);
    } else {
      node = new nodes.Comment(tok.val, tok.buffer);
    }

    node.line = this.line();
    return node;
  },

  /**
   * doctype
   */

  parseDoctype: function(){
    var tok = this.expect('doctype')
      , node = new nodes.Doctype(tok.val);
    node.line = this.line();
    return node;
  },

  /**
   * filter attrs? text-block
   */

  parseFilter: function(){
    var block
      , tok = this.expect('filter')
      , attrs = this.accept('attrs');

    this.lexer.pipeless = true;
    block = this.parseTextBlock();
    this.lexer.pipeless = false;

    var node = new nodes.Filter(tok.val, block, attrs && attrs.attrs);
    node.line = this.line();
    return node;
  },

  /**
   * each block
   */

  parseEach: function(){
    var tok = this.expect('each')
      , node = new nodes.Each(tok.code, tok.val, tok.key);
    node.line = this.line();
    node.block = this.block();
    if (this.peek().type == 'code' && this.peek().val == 'else') {
      this.advance();
      node.alternative = this.block();
    }
    return node;
  },

  /**
   * 'extends' name
   */

  parseExtends: function(){
    var path = require('path')
      , fs = require('fs')
      , dirname = path.dirname
      , basename = path.basename
      , join = path.join;

    if (!this.filename)
      throw new Error('the "filename" option is required to extend templates');

    var path = this.expect('extends').val.trim()
      , dir = dirname(this.filename);

    var path = join(dir, path + '.jade')
      , str = fs.readFileSync(path, 'utf8')
      , parser = new Parser(str, path, this.options);

    parser.blocks = this.blocks;
    parser.contexts = this.contexts;
    this.extending = parser;

    // TODO: null node
    return new nodes.Literal('');
  },

  /**
   * 'block' name block
   */

  parseBlock: function(){
    var block = this.expect('block')
      , mode = block.mode
      , name = block.val.trim();

    block = 'indent' == this.peek().type
      ? this.block()
      : new nodes.Block(new nodes.Literal(''));

    var prev = this.blocks[name];

    if (prev) {
      switch (prev.mode) {
        case 'append':
          block.nodes = block.nodes.concat(prev.nodes);
          prev = block;
          break;
        case 'prepend':
          block.nodes = prev.nodes.concat(block.nodes);
          prev = block;
          break;
      }
    }

    block.mode = mode;
    return this.blocks[name] = prev || block;
  },

  /**
   * include block?
   */

  parseInclude: function(){
    var path = require('path')
      , fs = require('fs')
      , dirname = path.dirname
      , basename = path.basename
      , join = path.join;

    var path = this.expect('include').val.trim()
      , dir = dirname(this.filename);

    if (!this.filename)
      throw new Error('the "filename" option is required to use includes');

    // no extension
    if (!~basename(path).indexOf('.')) {
      if (fs.existsSync(join(dir, path + '.jade'))) {
        path += '.jade';
      } else if (fs.existsSync(join(dir, path + '.jadec'))) {
        path += '.jadec';
      } else {
        // default back to .jade so the error will print with that path choice
        path += '.jade';
      }
    }

    // non-jade
    if ('.jade' != path.substr(-5) && '.jadec' != path.substr(-6)) {
      var path = join(dir, path);
      var str = fs.readFileSync(path, 'utf8');
      var ext = extname(path).slice(1);
      var filter = filters[ext];
      if (filter) str = filter(str);
      return new nodes.Literal(str);
    }

    var path = join(dir, path);
    var str = fs.readFileSync(path, 'utf8');
    var parser = new Parser(str, path, this.options);
    parser.blocks = utils.merge({}, this.blocks);
    parser.mixins = this.mixins;

    this.context(parser);
    var ast = parser.parse();
    this.context();
    ast.filename = path;

    if ('indent' == this.peek().type) {
      ast.includeBlock().push(this.block());
    }

    return ast;
  },

  /**
   * call ident block
   */

  parseCall: function(){
    var tok = this.expect('call')
      , name = tok.val
      , args = tok.args
      , mixin = new nodes.Mixin(name, args, new nodes.Block, true);

    this.tag(mixin);
    if (mixin.block.isEmpty()) mixin.block = null;
    return mixin;
  },

  /**
   * mixin block
   */

  parseMixin: function(){
    var tok = this.expect('mixin')
      , name = tok.val
      , args = tok.args
      , mixin;

    // definition
    if ('indent' == this.peek().type) {
      mixin = new nodes.Mixin(name, args, this.block(), false);
      this.mixins[name] = mixin;
      return mixin;
    // call
    } else {
      return new nodes.Mixin(name, args, null, true);
    }
  },

  /**
   * indent (text | newline)* outdent
   */

  parseTextBlock: function(){
    var block = new nodes.Block;
    block.line = this.line();
    var spaces = this.expect('indent').val;
    if (null == this._spaces) this._spaces = spaces;
    var indent = Array(spaces - this._spaces + 1).join(' ');
    while ('outdent' != this.peek().type) {
      switch (this.peek().type) {
        case 'newline':
          this.advance();
          break;
        case 'indent':
          this.parseTextBlock().nodes.forEach(function(node){
            block.push(node);
          });
          break;
        default:
          var text = new nodes.Text(indent + this.advance().val);
          text.line = this.line();
          block.push(text);
      }
    }

    if (spaces == this._spaces) this._spaces = null;
    this.expect('outdent');
    return block;
  },

  /**
   * indent expr* outdent
   */

  block: function(){
    var block = new nodes.Block;
    block.line = this.line();
    this.expect('indent');
    while ('outdent' != this.peek().type) {
      if ('newline' == this.peek().type) {
        this.advance();
      } else {
        block.push(this.parseExpr());
      }
    }
    this.expect('outdent');
    return block;
  },

  /**
   * interpolation (attrs | class | id)* (text | code | ':')? newline* block?
   */

  parseInterpolation: function(){
    var tok = this.advance();
    var tag = new nodes.Tag(tok.val);
    tag.buffer = true;
    return this.tag(tag);
  },

  /**
   * tag (attrs | class | id)* (text | code | ':')? newline* block?
   */

  parseTag: function(){
    // ast-filter look-ahead
    var i = 2;
    if ('attrs' == this.lookahead(i).type) ++i;

    var tok = this.advance()
      , tag = new nodes.Tag(tok.val);

    tag.selfClosing = tok.selfClosing;

    return this.tag(tag);
  },

  /**
   * Parse tag.
   */

  tag: function(tag){
    var dot;

    tag.line = this.line();

    // (attrs | class | id)*
    out:
      while (true) {
        switch (this.peek().type) {
          case 'id':
          case 'class':
            var tok = this.advance();
            tag.setAttribute(tok.type, "'" + tok.val + "'");
            continue;
          case 'attrs':
            var tok = this.advance()
              , obj = tok.attrs
              , escaped = tok.escaped
              , names = Object.keys(obj);

            if (tok.selfClosing) tag.selfClosing = true;

            for (var i = 0, len = names.length; i < len; ++i) {
              var name = names[i]
                , val = obj[name];
              tag.setAttribute(name, val, escaped[name]);
            }
            continue;
          default:
            break out;
        }
      }

    // check immediate '.'
    if ('.' == this.peek().val) {
      dot = tag.textOnly = true;
      this.advance();
    }

    // (text | code | ':')?
    switch (this.peek().type) {
      case 'text':
        tag.block.push(this.parseText());
        break;
      case 'code':
        tag.code = this.parseCode();
        break;
      case ':':
        this.advance();
        tag.block = new nodes.Block;
        tag.block.push(this.parseExpr());
        break;
    }

    // newline*
    while ('newline' == this.peek().type) this.advance();

    tag.textOnly = tag.textOnly || ~textOnly.indexOf(tag.name);

    // script special-case
    if ('script' == tag.name) {
      var type = tag.getAttribute('type');
      if (!dot && type && 'text/javascript' != type.replace(/^['"]|['"]$/g, '')) {
        tag.textOnly = false;
      }
    }

    // block?
    if ('indent' == this.peek().type) {
      if (tag.textOnly) {
        this.lexer.pipeless = true;
        tag.block = this.parseTextBlock();
        this.lexer.pipeless = false;
      } else {
        var block = this.block();
        if (tag.block) {
          for (var i = 0, len = block.nodes.length; i < len; ++i) {
            tag.block.push(block.nodes[i]);
          }
        } else {
          tag.block = block;
        }
      }
    }

    return tag;
  }
};
<|endoftext|>


<commit_before>'use strict';

function merge(query, next) {
	if (Array.isArray(query)) {
		return query.map(q => merge(q, next))
	}
	if (query.aggregator) {
		query.body.forEach(q => {
			merge(q, next);
		})
		return query;
	} else if (query.functions) {
		query.functions = query.functions.concat(next);
		return query;
	} else {
		return [query].concat(next)
	}
}

function analyseFunctionCall (func, body) {
	if (typeof body === 'string' && /^('|").*\1/.test(body)) {
		body = body.substr(1, body.length -2);
	}

	return func.charAt(0) === '@' ? {aggregator: func, body} : {func, body};
}

class Deconstructor {
	constructor (aggregator) {
		this.aggregator = aggregator;
		this.openSingleQuotes = 0;
		this.openDoubleQuotes = 0;
	}

	deconstructAggregator (str) {

		this.str = str;

		if (str.charAt(0) === '@') {
			return this.deconstruct(str)
		}

		const queries = [];
		while(this.str) {
			const str = this.str;
			this.str = null;
			queries.push(str.charAt(0) === '@' ? this.deconstruct(str) : this.deconstructQuery(str));
		}
		return queries;
	}

	deconstructQuery (str) {

		if (str.charAt(0) === '@') {
			return this.deconstructAggregator(str);
		}
		const firstCut = str.split('->');
		const eventName = firstCut.shift();
		str = firstCut.join('->');
		const functions = this.deconstruct(str);
		return {
			event: eventName,
			functions: functions
		}
	}

	deconstruct (str) {

		if (!str) {
			return str;
		}

		if (this.aggregator && str.indexOf(',') === 0) {
			this.str = str.substr(1);
			return;
		}

		str = str.replace(/^->/, '');

		let openParantheses = 0;
		let char;
		let func;
		let body;

		for (let i = 0, il = str.length; i < il; i++) {
			char = str.charAt(i);
			switch (char) {
				case '(':
					!this.openDoubleQuotes && !this.openSingleQuotes && openParantheses++;
					break;
				case ')':
					!this.openDoubleQuotes && !this.openSingleQuotes && openParantheses--;
					break;
				case '"':
					!this.openSingleQuotes && (this.openDoubleQuotes = Math.abs(this.openDoubleQuotes - 1));
					break;
				case '\'':
					!this.openDoubleQuotes && (this.openSingleQuotes = Math.abs(this.openSingleQuotes - 1));
					break;
				case 'default':
					break;
			}

			if (openParantheses && !func) {
				func = str.substr(0, i);
			} else if (func && !openParantheses && !body) {
				body = str.substr(func.length + 1, i - (func.length + 1))
				if (func.charAt(0) === '@') {
					body = new Deconstructor(func.substr(1)).deconstructAggregator(body);
				}
				let res = analyseFunctionCall(func, body);
				const next = this.deconstruct(str.substr(i + 1), this.openSingleQuotes, this.openDoubleQuotes)
				return next ? merge(res, next) : res.aggregator ? res : [res]
			}
		}
		return str;
	}
}
module.exports = function deconstruct (str) {
	return new Deconstructor().deconstructQuery(str);
}
<commit_msg>handle printing<commit_after>'use strict';

function merge(query, next) {
	if (Array.isArray(query)) {
		return query.map(q => merge(q, next))
	}
	if (query.aggregator) {
		query.body.forEach(q => {
			merge(q, next);
		})
		return query;
	} else if (query.functions) {
		query.functions = query.functions.concat(next);
		return query;
	} else {
		return [query].concat(next)
	}
}

function analyseFunctionCall (func, body) {
	if (typeof body === 'string' && /^('|").*\1/.test(body)) {
		body = body.substr(1, body.length -2);
	}

	return func.charAt(0) === '@' ? {aggregator: func, body} : {func, body};
}

class Deconstructor {
	constructor (aggregator) {
		this.aggregator = aggregator;
		this.openSingleQuotes = 0;
		this.openDoubleQuotes = 0;
	}

	deconstructAggregator (str) {

		this.str = str;

		if (str.charAt(0) === '@') {
			const query = this.deconstruct(str);
			query.print = this.topLevel && this.print
			return query;
		}

		const queries = [];
		while(this.str) {
			const str = this.str;
			this.str = null;
			queries.push(str.charAt(0) === '@' ? this.deconstruct(str) : this.deconstructQuery(str));
		}
		return queries;
	}

	deconstructQuery (str) {

		if (str.charAt(0) === '@') {
			return this.deconstructAggregator(str);
		}
		const firstCut = str.split('->');
		const eventName = firstCut.shift();
		str = firstCut.join('->');
		const functions = this.deconstruct(str);
		return {
			event: eventName,
			functions: functions,
			print: this.topLevel && this.print
		}
	}

	deconstruct (str) {

		if (!str) {
			return str;
		}

		if (this.aggregator && str.indexOf(',') === 0) {
			this.str = str.substr(1);
			return;
		}

		str = str.replace(/^->/, '');

		let openParantheses = 0;
		let char;
		let func;
		let body;

		for (let i = 0, il = str.length; i < il; i++) {
			char = str.charAt(i);
			switch (char) {
				case '(':
					!this.openDoubleQuotes && !this.openSingleQuotes && openParantheses++;
					break;
				case ')':
					!this.openDoubleQuotes && !this.openSingleQuotes && openParantheses--;
					break;
				case '"':
					!this.openSingleQuotes && (this.openDoubleQuotes = Math.abs(this.openDoubleQuotes - 1));
					break;
				case '\'':
					!this.openDoubleQuotes && (this.openSingleQuotes = Math.abs(this.openSingleQuotes - 1));
					break;
				case 'default':
					break;
			}

			if (openParantheses && !func) {
				func = str.substr(0, i);
			} else if (func && !openParantheses && !body) {
				body = str.substr(func.length + 1, i - (func.length + 1))
				if (func.charAt(0) === '@') {
					body = new Deconstructor(func.substr(1)).deconstructAggregator(body);
				}
				if (func === 'print') {
					this.print = body;
				}
				let res = analyseFunctionCall(func, body);
				const next = this.deconstruct(str.substr(i + 1), this.openSingleQuotes, this.openDoubleQuotes)
				return next ? merge(res, next) : res.aggregator ? res : [res]
			}
		}
		return str;
	}
}
module.exports = function deconstruct (str) {
	const deconstructor = new Deconstructor();
	deconstructor.topLevel = true;
	return deconstructor.deconstructQuery(str);
}
<|endoftext|>


<commit_before>var vm = require('vm')
var util = require('util')
var _ = require('underscore')

module.exports = {
  stateJcssVars: require('../helpers/helpers.js'),

  exec: function (javaScriptString) {
    var returnString = ''
    var sandbox = {
      $: this.stateJcssVars,
      returnString: '',
    }
    function css (cssString) {
      returnString += cssString
    }
    try {
      vm.runInNewContext(javaScriptString + '\n' + css.toString(), sandbox)
      this.stateJcssVars = sandbox.$
      return sandbox.returnString
    } catch (e) {
      console.log('Exception in your jcss')
      console.log(javaScriptString)
      console.log(e)
      return false
    }
  },

  parseCssVars: function (cssString) {
    /*
      \$\.
      [\w\,\.\d\_\-\$]+
      (
        \(
        [^\(\)]*
        \)
      )*
    */
    var self = this
    var varRegex = /\$\.[\w\,\.\d\_\-\$]+(\([^\(\)]*\))*/gm
    if(!cssString) return '';
    cssString = cssString.replace(varRegex, function (match) {
      var $ = self.stateJcssVars
      return eval(match)
    })
    return cssString
  },

  split: function (jcssString) {
    /*
      ^[
          \s\t
        ]*
        (
          (
            (
              (
                [\.\#]?
                [\w\-_\d]+
              )
              |
              \*
            )
            |
            (
              (
                \[
                |
                \:{1,2}
              ){1}
              [\w\-_\d\*\^\~\|\=\"\$]+
              (
                \([^\)\;]+\)
              )?
              \]?
            )
          )+
          [\n\,\s\>\+\~]*
        )+
      \{
      [^\}]*
      \}
      \n*
    */
    var cssRegex = /^[\s\t]*(((([\.\#]?[\w\-_\d]+)|\*)|((\[|\:{1,2}){1}[\w\-_\d\*\^\~\|\=\"\$]+(\([^\)\;]+\))?\]?))+[\n\,\s\>\+\~]*)+\{[^\}]*\}\n*/gm
    var cssRegexRes, returnLang
    if(!jcssString) return false
    cssRegexRes = cssRegex.exec(jcssString)
    if(!cssRegexRes) return ['js', jcssString, false]
    returnLang = jcssString.indexOf(cssRegexRes[0]) === 0 ? 'css' : 'js'
    return [returnLang].concat(this.splitAroundSubstring(cssRegexRes[0], jcssString))
  },

  splitAroundSubstring: function (substring, string) {
    var substringFirst = string.indexOf(substring)
    var substringLast = substringFirst + substring.length
    if(substringFirst === 0) {
      return [string.substr(0, substringLast), string.substr(substringLast)]
    } else {
      return [string.substr(0, substringFirst), string.substr(substringFirst)]
    }
  },

  interpret: function (someArr) {
    return (someArr[0] == 'js' ? this.exec(someArr[1]) : this.parseCssVars(someArr[1]))
  },

  parse: function (jcssString) {
    var splitJcss = []
    var buffer = ''
    var parsedCss = ''
    if(typeof jcssString != 'string') return false
    do {
      splitJcss = this.split(jcssString)
      parsedCss += (buffer = this.interpret(splitJcss))
      if(buffer === false) return false
    } while (jcssString = splitJcss[2])
    return parsedCss
  }
}
<commit_msg>Pass all tests for parser.funcsToString<commit_after>var vm = require('vm')
var util = require('util')
var _ = require('underscore')

var utilities = require('../lib/utilities.js')

module.exports = {
  stateJcssVars: require('../helpers/helpers.js'),

  funcsToString: function (funcArr) {
    var returnString
    funcArr = funcArr.map(utilities.caller('toString'))
    console.log(funcArr)
    return funcArr.join('\n')
  },

  exec: function (javaScriptString) {
    var returnString = ''
    var sandbox = {
      $: this.stateJcssVars,
      returnString: '',
    }
    function css (cssString) {
      returnString += cssString
    }
    try {
      vm.runInNewContext(javaScriptString + '\n' + css.toString(), sandbox)
      this.stateJcssVars = sandbox.$
      return sandbox.returnString
    } catch (e) {
      console.log('Exception in your jcss')
      console.log(javaScriptString)
      console.log(e)
      return false
    }
  },

  parseCssVars: function (cssString) {
    /*
      \$\.
      [\w\,\.\d\_\-\$]+
      (
        \(
        [^\(\)]*
        \)
      )*
    */
    var self = this
    var varRegex = /\$\.[\w\,\.\d\_\-\$]+(\([^\(\)]*\))*/gm
    if(!cssString) return '';
    cssString = cssString.replace(varRegex, function (match) {
      var $ = self.stateJcssVars
      return eval(match)
    })
    return cssString
  },

  split: function (jcssString) {
    /*
      ^[
          \s\t
        ]*
        (
          (
            (
              (
                [\.\#]?
                [\w\-_\d]+
              )
              |
              \*
            )
            |
            (
              (
                \[
                |
                \:{1,2}
              ){1}
              [\w\-_\d\*\^\~\|\=\"\$]+
              (
                \([^\)\;]+\)
              )?
              \]?
            )
          )+
          [\n\,\s\>\+\~]*
        )+
      \{
      [^\}]*
      \}
      \n*
    */
    var cssRegex = /^[\s\t]*(((([\.\#]?[\w\-_\d]+)|\*)|((\[|\:{1,2}){1}[\w\-_\d\*\^\~\|\=\"\$]+(\([^\)\;]+\))?\]?))+[\n\,\s\>\+\~]*)+\{[^\}]*\}\n*/gm
    var cssRegexRes, returnLang
    if(!jcssString) return false
    cssRegexRes = cssRegex.exec(jcssString)
    if(!cssRegexRes) return ['js', jcssString, false]
    returnLang = jcssString.indexOf(cssRegexRes[0]) === 0 ? 'css' : 'js'
    return [returnLang].concat(this.splitAroundSubstring(cssRegexRes[0], jcssString))
  },

  splitAroundSubstring: function (substring, string) {
    var substringFirst = string.indexOf(substring)
    var substringLast = substringFirst + substring.length
    if(substringFirst === 0) {
      return [string.substr(0, substringLast), string.substr(substringLast)]
    } else {
      return [string.substr(0, substringFirst), string.substr(substringFirst)]
    }
  },

  interpret: function (someArr) {
    return (someArr[0] == 'js' ? this.exec(someArr[1]) : this.parseCssVars(someArr[1]))
  },

  parse: function (jcssString) {
    var splitJcss = []
    var buffer = ''
    var parsedCss = ''
    if(typeof jcssString != 'string') return false
    do {
      splitJcss = this.split(jcssString)
      parsedCss += (buffer = this.interpret(splitJcss))
      if(buffer === false) return false
    } while (jcssString = splitJcss[2])
    return parsedCss
  }
}
<|endoftext|>


<commit_before>var assert = require("assert");
var types = require("./types");
var n = types.namedTypes;
var b = types.builders;
var isObject = types.builtInTypes.object;
var isArray = types.builtInTypes.array;
var isFunction = types.builtInTypes.function;
var Patcher = require("./patcher").Patcher;
var normalizeOptions = require("./options").normalize;
var hasOwn = Object.prototype.hasOwnProperty;

exports.parse = function parse(source, options) {
    options = normalizeOptions(options);

    var lines = require("./lines").fromString(source, options);

    var pure = options.esprima.parse(lines.toString({
        tabWidth: options.tabWidth,
        reuseWhitespace: false,
        useTabs: false
    }), {
        loc: true,
        range: options.range,
        comment: true,
        tolerant: options.tolerant
    });

    new LocationFixer(lines).fix(pure);

    require("./comments").add(pure, lines);

    // In order to ensure we reprint leading and trailing program
    // comments, wrap the original Program node with a File node.
    pure = b.file(pure);
    pure.loc = {
        lines: lines,
        indent: 0,
        start: lines.firstPos(),
        end: lines.lastPos()
    };

    // Return a copy of the original AST so that any changes made may be
    // compared to the original.
    return copyAst(pure);
};

function LocationFixer(lines) {
    assert.ok(this instanceof LocationFixer);
    this.lines = lines;
    this.indent = 0;
}

var LFp = LocationFixer.prototype;

LFp.fix = function(node) {
    if (isArray.check(node)) {
        node.forEach(this.fix, this);
        return;
    }

    if (!isObject.check(node)) {
        return;
    }

    var lines = this.lines;
    var loc = node && node.loc;
    var start = loc && loc.start;
    var end = loc && loc.end;
    var oldIndent = this.indent;
    var newIndent = oldIndent;

    if (start) {
        start.line = Math.max(start.line, 1);

        if (lines.isPrecededOnlyByWhitespace(start)) {
            // The indent returned by lines.getIndentAt is the column of
            // the first non-space character in the line, but start.column
            // may fall before that character, as when a file begins with
            // whitespace but its start.column nevertheless must be 0.
            assert.ok(start.column <= lines.getIndentAt(start.line));
            newIndent = this.indent = start.column;
        }
    }

    var names = types.getFieldNames(node);
    for (var i = 0, len = names.length; i < len; ++i) {
        this.fix(node[names[i]]);
    }

    // Restore original value of this.indent after the recursive call.
    this.indent = oldIndent;

    if (loc) {
        loc.lines = lines;
        loc.indent = newIndent;
    }

    if (end) {
        end.line = Math.max(end.line, 1);

        var pos = {
            line: end.line,
            column: end.column
        };

        // Negative columns might indicate an Esprima bug?
        // For now, treat them as reverse indices, a la Python.
        if (pos.column < 0)
            pos.column += lines.getLineLength(pos.line);

        while (lines.prevPos(pos)) {
            if (/\S/.test(lines.charAt(pos))) {
                assert.ok(lines.nextPos(pos));

                end.line = pos.line;
                end.column = pos.column;

                break;
            }
        }
    }

    if (n.Property.check(node) && (node.method || node.shorthand)) {
        // If the Property is a .method or .shorthand property, then the
        // location information stored in node.value.loc is very likely
        // untrustworthy (just the {body} part of a method, or nothing in
        // the case of shorthand properties), so we null out that
        // information to prevent accidental reuse of bogus source code
        // during reprinting.
        node.value.loc = null;
    }
};

function copyAst(node) {
    if (typeof node !== "object") {
        return node;
    }

    if (isObject.check(node)) {
        var copy = Object.create(Object.getPrototypeOf(node), {
            original: { // Provide a link from the copy to the original.
                value: node,
                configurable: false,
                enumerable: false,
                writable: true
            }
        });

        for (var key in node) {
            var val = node[key];
            if (val && key === "loc") {
                copy.loc = {
                    start: { line: val.start.line, column: val.start.column },
                    end: { line: val.end.line, column: val.end.column }
                };
            } else if (hasOwn.call(node, key)) {
                copy[key] = copyAst(val);
            }
        }

        return copy;
    }

    if (isArray.check(node)) {
        return node.map(copyAst);
    }

    return node;
}
<commit_msg>Mistrust .loc information for MethodDefinition nodes too.<commit_after>var assert = require("assert");
var types = require("./types");
var n = types.namedTypes;
var b = types.builders;
var isObject = types.builtInTypes.object;
var isArray = types.builtInTypes.array;
var isFunction = types.builtInTypes.function;
var Patcher = require("./patcher").Patcher;
var normalizeOptions = require("./options").normalize;
var hasOwn = Object.prototype.hasOwnProperty;

exports.parse = function parse(source, options) {
    options = normalizeOptions(options);

    var lines = require("./lines").fromString(source, options);

    var pure = options.esprima.parse(lines.toString({
        tabWidth: options.tabWidth,
        reuseWhitespace: false,
        useTabs: false
    }), {
        loc: true,
        range: options.range,
        comment: true,
        tolerant: options.tolerant
    });

    new LocationFixer(lines).fix(pure);

    require("./comments").add(pure, lines);

    // In order to ensure we reprint leading and trailing program
    // comments, wrap the original Program node with a File node.
    pure = b.file(pure);
    pure.loc = {
        lines: lines,
        indent: 0,
        start: lines.firstPos(),
        end: lines.lastPos()
    };

    // Return a copy of the original AST so that any changes made may be
    // compared to the original.
    return copyAst(pure);
};

function LocationFixer(lines) {
    assert.ok(this instanceof LocationFixer);
    this.lines = lines;
    this.indent = 0;
}

var LFp = LocationFixer.prototype;

LFp.fix = function(node) {
    if (isArray.check(node)) {
        node.forEach(this.fix, this);
        return;
    }

    if (!isObject.check(node)) {
        return;
    }

    var lines = this.lines;
    var loc = node && node.loc;
    var start = loc && loc.start;
    var end = loc && loc.end;
    var oldIndent = this.indent;
    var newIndent = oldIndent;

    if (start) {
        start.line = Math.max(start.line, 1);

        if (lines.isPrecededOnlyByWhitespace(start)) {
            // The indent returned by lines.getIndentAt is the column of
            // the first non-space character in the line, but start.column
            // may fall before that character, as when a file begins with
            // whitespace but its start.column nevertheless must be 0.
            assert.ok(start.column <= lines.getIndentAt(start.line));
            newIndent = this.indent = start.column;
        }
    }

    var names = types.getFieldNames(node);
    for (var i = 0, len = names.length; i < len; ++i) {
        this.fix(node[names[i]]);
    }

    // Restore original value of this.indent after the recursive call.
    this.indent = oldIndent;

    if (loc) {
        loc.lines = lines;
        loc.indent = newIndent;
    }

    if (end) {
        end.line = Math.max(end.line, 1);

        var pos = {
            line: end.line,
            column: end.column
        };

        // Negative columns might indicate an Esprima bug?
        // For now, treat them as reverse indices, a la Python.
        if (pos.column < 0)
            pos.column += lines.getLineLength(pos.line);

        while (lines.prevPos(pos)) {
            if (/\S/.test(lines.charAt(pos))) {
                assert.ok(lines.nextPos(pos));

                end.line = pos.line;
                end.column = pos.column;

                break;
            }
        }
    }

    if ((n.MethodDefinition && n.MethodDefinition.check(node)) ||
        (n.Property.check(node) && (node.method || node.shorthand))) {
        // If the node is a MethodDefinition or a .method or .shorthand
        // Property, then the location information stored in
        // node.value.loc is very likely untrustworthy (just the {body}
        // part of a method, or nothing in the case of shorthand
        // properties), so we null out that information to prevent
        // accidental reuse of bogus source code during reprinting.
        node.value.loc = null;
    }
};

function copyAst(node) {
    if (typeof node !== "object") {
        return node;
    }

    if (isObject.check(node)) {
        var copy = Object.create(Object.getPrototypeOf(node), {
            original: { // Provide a link from the copy to the original.
                value: node,
                configurable: false,
                enumerable: false,
                writable: true
            }
        });

        for (var key in node) {
            var val = node[key];
            if (val && key === "loc") {
                copy.loc = {
                    start: { line: val.start.line, column: val.start.column },
                    end: { line: val.end.line, column: val.end.column }
                };
            } else if (hasOwn.call(node, key)) {
                copy[key] = copyAst(val);
            }
        }

        return copy;
    }

    if (isArray.check(node)) {
        return node.map(copyAst);
    }

    return node;
}
<|endoftext|>


<commit_before>'use strict'

const _ = require('lodash')
const Handlebars = require('handlebars')

module.exports = class Template {
  parseVariables (content) {
    return this.parseAst(Handlebars.parse(content).body)
  }

  storeVariable (obj, variable) {
    if (!obj[variable.name]) {
      obj[variable.name] = {
        value: null,
        description: null
      }
    }

    let value = obj[variable.name].value
    let description = obj[variable.name].description

    if (_.isUndefined(value) || _.isNull(value)) {
      obj[variable.name].value = variable.value
    }

    if (_.isUndefined(description) || _.isNull(description)) {
      obj[variable.name].description = variable.description
    }
  }

  parseAst (ast) {
    let retObj = {}

    for (let node of ast) {
      let variable = null

      switch (node.type) {
        case 'CommentStatement':
          variable = this.parseComment(node)
          break
        case 'MustacheStatement':
          variable = this.parseMustache(node)
          break
        case 'BlockStatement':
          variable = this.parseBlock(node)
          break
      }

      if (variable) {
        this.storeVariable(retObj, variable)
      }

      if (node.program && node.program.body.length) {
        let subAst = this.parseAst(node.program.body)

        for (let variableName in subAst) {
          this.storeVariable(retObj, {
            name: variableName,
            value: subAst[variableName].value,
            description: subAst[variableName].description
          })
        }
      }
    }

    return retObj
  }

  parseComment (node) {
    let str = node.value
    let value = null
    let description = null

    if (_.includes(str, '#')) {
      let split = str.split('#')
      description = _.trim(_.last(split))
      str = _.first(split)
    }

    if (_.includes(str, '=')) {
      let split = str.split('=')
      value = _.trim(_.last(split))
      str = _.first(split)

      if (value.toLowerCase() === 'true') {
        value = true
      } else if (value.toLowerCase() === 'false') {
        value = false
      }

      if (!isNaN(value) && !_.isBoolean(value)) {
        value = _.toNumber(value)
      }
    }

    if (value || description) {
      return { name: _.trim(str), value, description }
    } else {
      return null
    }
  }

  parseMustache (node) {
    let name = node.path.original

    if (node.params.length) { // has a helper(s) in front of the variable name
      name = _.last(node.params).original
    }

    return { name, value: null, description: null }
  }

  parseBlock (node) {
    let name = node.params[0].original
    let value = null

    switch (node.path.original.toLowerCase()) {
      case 'if':
        value = true
        break
      case 'unless':
        value = false
        break
      default:
        return null
    }

    return { name, value, description: null }
  }
}
<commit_msg>no need to check for undefined<commit_after>'use strict'

const _ = require('lodash')
const Handlebars = require('handlebars')

module.exports = class Template {
  parseVariables (content) {
    return this.parseAst(Handlebars.parse(content).body)
  }

  storeVariable (obj, variable) {
    if (!obj[variable.name]) {
      obj[variable.name] = {
        value: null,
        description: null
      }
    }

    if (_.isNull(obj[variable.name].value)) {
      obj[variable.name].value = variable.value
    }

    if (_.isNull(obj[variable.name].description)) {
      obj[variable.name].description = variable.description
    }
  }

  parseAst (ast) {
    let retObj = {}

    for (let node of ast) {
      let variable = null

      switch (node.type) {
        case 'CommentStatement':
          variable = this.parseComment(node)
          break
        case 'MustacheStatement':
          variable = this.parseMustache(node)
          break
        case 'BlockStatement':
          variable = this.parseBlock(node)
          break
      }

      if (variable) {
        this.storeVariable(retObj, variable)
      }

      if (node.program && node.program.body.length) {
        let subAst = this.parseAst(node.program.body)

        for (let variableName in subAst) {
          this.storeVariable(retObj, {
            name: variableName,
            value: subAst[variableName].value,
            description: subAst[variableName].description
          })
        }
      }
    }

    return retObj
  }

  parseComment (node) {
    let str = node.value
    let value = null
    let description = null

    if (_.includes(str, '#')) {
      let split = str.split('#')
      description = _.trim(_.last(split))
      str = _.first(split)
    }

    if (_.includes(str, '=')) {
      let split = str.split('=')
      value = _.trim(_.last(split))
      str = _.first(split)

      if (value.toLowerCase() === 'true') {
        value = true
      } else if (value.toLowerCase() === 'false') {
        value = false
      }

      if (!isNaN(value) && !_.isBoolean(value)) {
        value = _.toNumber(value)
      }
    }

    if (value || description) {
      return { name: _.trim(str), value, description }
    } else {
      return null
    }
  }

  parseMustache (node) {
    let name = node.path.original

    if (node.params.length) { // has a helper(s) in front of the variable name
      name = _.last(node.params).original
    }

    return { name, value: null, description: null }
  }

  parseBlock (node) {
    let name = node.params[0].original
    let value = null

    switch (node.path.original.toLowerCase()) {
      case 'if':
        value = true
        break
      case 'unless':
        value = false
        break
      default:
        return null
    }

    return { name, value, description: null }
  }
}
<|endoftext|>


<commit_before>
var path    = require('path');
var fs      = require('fs');
var crypto  = require('crypto');
var http    = require('http');
var https   = require('https');

var PAYBOX_SERVERS          = require('./servers.json');
var PAYBOX_RESPONSE_ERRORS  = require('./response-errors.json');

var paybox = {
  /**
   * Checks identity of a response and if paybox returned an error
   * @param  {Object}   transaction   Transaction object returned by paybox.createTransaction()
   * @param  {Object}   datas         Datas received from Paybox
   * @param  {String}   pubkeyPath    The path where Paybox public key is stored on the server
   * @param  {Function} callback      Function to be called when checks are finished. Arguments are (error, transaction)
   * @return {void}
   */
  response          : function(transaction, datas, pubkeyPath, callback){
    var _error = null;
    paybox.checkIdentity(transaction, datas, pubkeyPath, function(identityOK){
      if(!identityOK){
        _error = 'This response is not from a paybox server';
      }
      else{
        _error = paybox.getResponseError(transaction, datas);
      }
      callback(_error, transaction);
    });
  },
  /**
   * Checks if there is an error in response datas
   * @param  {Object}   transaction   Transaction object returned by paybox.createTransaction()
   * @param  {Object}   datas         Datas received from Paybox
   * @return {String | NULL}          NULL if there is no error or a string explaining the error
   */
  getResponseError  : function(transaction, datas){
    var _error = null;
    var _errorField = paybox.getErrorField(transaction);
    var _errorCode = datas[_errorField];
    if(_errorCode !== '00000'){
      Object.keys(PAYBOX_RESPONSE_ERRORS).map(function(error){
        var _regExp = new RegExp(error, 'g');
        if(_regExp.test(_errorCode)){
          _error = PAYBOX_RESPONSE_ERRORS[error].replace(/%ERROR_CODE%/g, _errorCode);
        }
      });
      if(_error === null){
        _error = 'Unknown error returned by paybox : ' + _errorCode;
      }
    }
    return _error;
  },
  getKey            : function(pathOrKey, callback){
    if(pathOrKey.charAt(0) === '/'){
      fs.readFile(pathOrKey, 'utf8', function(err, key){
        callback(key);
      });
    }
    else{
      callback(pathOrKey);
    }
  },
  /**
   * Checks if a query is from Paybox
   * @param  {Object}   transaction   Transaction object returned by paybox.createTransaction()
   * @param  {Object}   datas         Datas received from Paybox
   * @param  {String}   pubkeyPath    The path where Paybox public key is stored on the server
   * @return {Boolean}
   */
  checkIdentity     : function(transaction, datas, pubkeyPath, callback){
    paybox.getKey(pubkeyPath, function(pubkey){
      /*console.log(pubkey);*/
      /*var pubkey     = fs.readFileSync(path.resolve(pubkeyPath), 'utf8');*/
      var _signField  = paybox.getSignField(transaction);
      var _check = true;
      if(_signField !== null){
        var _sign = datas[_signField];
        var _message = [];
        Object.keys(datas).map(function(field){
          if(field !== _signField){
            _message.push(field + '=' + encodeURIComponent(datas[field]));
          }
        });
        _message = _message.join('&');
        _check = paybox.checkSignature(_message, _sign, pubkey);
      }
      callback(_check);
    });
  },
  /**
   * Checks if a message and its signature match with the given public key
   * @param  {String} message   The message with format "field1=val1&field2=val2..."
   * @param  {String} signature The RSA SHA1 signature of the message
   * @param  {String} pubkey    The public key in UTF8
   * @return {Boolean}          If the message is signed by the owner of the public key
   */
  checkSignature    : function(message, signature, pubkey){
    signature = new Buffer(decodeURIComponent(signature), 'base64');
    var check = crypto.createVerify('SHA1');
    check.update(message);
    return check.verify(pubkey, signature);
  },
  /**
   * Generates a signature and inserts it
   * @param  {Object} transaction The transaction returned by paybox.createTransaction()
   * @param  {String} key         Your private key in HEX format
   * @return {Object}             The transaction signed
   */
  signTransaction   : function(transaction, key, callback){
    var error = null;
    transaction['PBX_'].TIME = (new Date()).toISOString();
    transaction['PBX_'].HASH = 'SHA512';
    var _hmac = paybox.generateHMAC(transaction['PBX_'], key);
    transaction['PBX_'].HMAC = _hmac;
    if(_hmac === null){
      error = 'Bad private key format, unable to generate signature';
    }
    callback(error, transaction);
  },
  /**
   * Generates all inputs of the form to submit to Paybox and inserts them in the transaction
   * @param  {Object} transaction The transaction returned by paybox.createTransaction()
   * @return {Object}             The transaction modified
   */
  generateFormBody  : function(transaction){
    transaction.body = Object.keys(transaction['PBX_']).map(function(field){
      return '<input type="hidden" name="PBX_' + field + '" value="' + transaction['PBX_'][field] + '">';
    }).join('');
    return transaction;
  },
  /**
   * Generate an HMAC signature for given PBX fields
   * @param  {Object} PBXFields Paybox parameters without 'PBX_' prefix
   * @param  {String} key       The HEX format of the private key
   * @return {String}           Uppercase HEX format of the HMAC
   */
  generateHMAC      : function(PBXFields, key){
    var _hmac = null;
    try{
      var _message = Object.keys(PBXFields).map(function(field){
        return 'PBX_' + field + '=' + PBXFields[field];
      }).join('&');
      _hmac = paybox.generateSignature(_message, key);
    }
    catch(e){}
    return _hmac;
  },
  generateSignature : function(message, key){
    var _signature = null;
    try{
      var _key = (new Buffer(key, 'hex')).toString('binary');
      _signature = crypto.createHmac('sha512', _key).update(message).digest('hex').toUpperCase();
    }
    catch(e){}
    return _signature;
  },
  /**
   * Finds the field name for the signature in the transaction
   * @param  {Ovject}         transaction   The transaction returned by paybox.createTransaction()
   * @return {String | null}                The field name or NULL if the signature was not asked in the transaction
   */
  getSignField      : function(transaction){
    return paybox.getFieldName(transaction, 'K');
  },
  getErrorField     : function(transaction){
    return paybox.getFieldName(transaction, 'E');
  },
  getFieldName      : function(transaction, code){
    var _pbxRetour = transaction['PBX_'].RETOUR;
    var _fieldsRegExp = new RegExp('^.*;([^;]+):' + code.toUpperCase() + '[;]?.*', 'g');
    var _hasField = _fieldsRegExp.test(_pbxRetour);
    return _hasField ? _pbxRetour.replace(_fieldsRegExp, '$1') : null;
  },
  /**
   * Creates a transaction object with all needed informations to query paybox server
   * @param  {Object}   options  Options to create the transaction
   * @param  {Function} callback Function to be called when the transaction will be created. Arguments are (error, transaction)
   * @return {void}
   */
  createTransaction : function(options, callback){
    paybox.getURL(options.offer, options.isTest === true, function(err, url){
      if(err !== null){
        return callback(err, null);
      }
      var _transaction = {
        'url'         : url,
        'expectedIP'  : '',
        'method'      : options.method || 'GET',
        'body'        : '',
        'PBX_'      : {
          'RUF1'        : 'POST'
        }
      };

      Object.keys(options['PBX_']).map(function(field){
        _transaction['PBX_'][field.toUpperCase()] = options['PBX_'][field];
      });

      paybox.signTransaction(_transaction, options.key, function(error, transaction){
        if(error === null){
          transaction = paybox.generateFormBody(transaction);
        }
        callback(error, transaction);
      });
    });
  },
  /**
   * Extracts informations from an URL formated in a string
   * @param  {String} serverURL The URL with all informations like port, path, host, protocol (only http or https) (ex : https://domain.com:3021/my/path)
   * @return {Object}           An object with all informations extracted from the string
   */
  extractURLInfos   : function(serverURL){
    var _infos = {
      isSSL : false,
      port  : 80,
      path  : '/load.html',
      host  : ''
    };
    _infos.host = serverURL
      .replace(/^(https?):\/\//g, function(p, protocol){
        _infos.isSSL  = protocol === 'https';
        _infos.port   = 443;
        return '';
      })
      .replace(/([^:|^\/]+)(.*)$/g, function(p, host, portAndPath){
        portAndPath
          .replace(/^:(\d+)/g, function(p, port){
            _infos.port = parseInt(port, 10);
            return '';
          })
          .replace(/^:?(\/.*)/g, function(p, path){
            _infos.path = path;
            return '';
          });
        return host;
      });
    return _infos;
  },
  /**
   * Recursive function to check all servers URL given
   * @param  {Array}    servers  List of URLs to check
   * @param  {Integer}  index    Current server index to check in servers Array
   * @param  {Function} callback Function to be called when checks are finished. 2 arguments : err, serverURL
   * @return {void}
   */
  checkNextServer   : function(servers, index, callback){
    var _serverURL = servers[index];
    paybox.checkServer(_serverURL, function(isAlive){
      if(isAlive){
        callback(null, _serverURL);
      }
      else if(++index < servers.length){
        paybox.checkNextServer(servers, index, callback);
      }
      else{
        callback('No alive server found');
      }
    });
  },
  /**
   * Makes a request to the given URL and check for a div#server_status and its content to be OK
   * @param  {String}   serverURL The URL with all needed parameters like port, path, host, protocol (only http or https)
   * @param  {Function} callback  Function to be called when the check is finished with 1 argument : Boolean isAlive
   * @return {void}
   */
  checkServer       : function(serverURL, callback){
    if(serverURL === undefined){
      return callback(false);
    }
    var _server     = paybox.extractURLInfos(serverURL);
    var reqLibrary  = _server.isSSL ? https : http;
    var req = reqLibrary.request(_server);
    req.on('response', function(res){
      var _isAlive = false;
      res.setEncoding('utf8');
      res.on('data', function(body){
        body = body.replace(/< *br *\/? *>/g, '').replace(/\n| /g, '');
        _isAlive = (/id="server_status"/g).test(body) && (/>OK<\/div>/g).test(body);
      });
      res.on('end', function(){
        callback(_isAlive);
      });
    });
    req.on('error', function(err){
      callback(false);
    });
    req.end();
  },
  /**
   * Checks every server for given offer and return a valid one to the callback
   * @param  {String}             offer     The offer we want server URL
   * @param  {optional Boolean}   isTest    If it has to return a test URL
   * @param  {Function}           callback  Function to be called when checks are finished. 2 arguments : err, serverURL
   * @return {void}
   */
  getURL            : function(offer, isTest, callback){
    if(callback === undefined){
      callback  = isTest;
      isTest    = false;
    }
    var _payboxSystemPath = '/cgi/MYchoix_pagepaiement.cgi';
    var servers = paybox.servers(offer, isTest);
    paybox.checkNextServer(servers, 0, function(err, serverURL){
      if(serverURL !== undefined){
        serverURL += _payboxSystemPath;
      }
      callback(err, serverURL);
    });
  },
  /**
   * Returns array of URLs for given offer
   * @param  {String}             offer   The offer the transaction is for. Eg "system"
   * @param  {optional Boolean}   test    If URLs needed are test servers. Default false.
   * @return {Array}                      List of servers URLs
   */
  servers           : function(offer, test){
    var _serversURLs = [];
    if(PAYBOX_SERVERS[offer] !== undefined){
      _serversURLs = PAYBOX_SERVERS[offer][test ? 'test' : 'prod'];
    }
    return _serversURLs;
  }
};

module.exports = paybox;
<commit_msg>correct node-paybox with node version >= 6.0.0<commit_after>
var path    = require('path');
var fs      = require('fs');
var crypto  = require('crypto');
var http    = require('http');
var https   = require('https');

var PAYBOX_SERVERS          = require('./servers.json');
var PAYBOX_RESPONSE_ERRORS  = require('./response-errors.json');

var paybox = {
  /**
   * Checks identity of a response and if paybox returned an error
   * @param  {Object}   transaction   Transaction object returned by paybox.createTransaction()
   * @param  {Object}   datas         Datas received from Paybox
   * @param  {String}   pubkeyPath    The path where Paybox public key is stored on the server
   * @param  {Function} callback      Function to be called when checks are finished. Arguments are (error, transaction)
   * @return {void}
   */
  response          : function(transaction, datas, pubkeyPath, callback){
    var _error = null;
    paybox.checkIdentity(transaction, datas, pubkeyPath, function(identityOK){
      if(!identityOK){
        _error = 'This response is not from a paybox server';
      }
      else{
        _error = paybox.getResponseError(transaction, datas);
      }
      callback(_error, transaction);
    });
  },
  /**
   * Checks if there is an error in response datas
   * @param  {Object}   transaction   Transaction object returned by paybox.createTransaction()
   * @param  {Object}   datas         Datas received from Paybox
   * @return {String | NULL}          NULL if there is no error or a string explaining the error
   */
  getResponseError  : function(transaction, datas){
    var _error = null;
    var _errorField = paybox.getErrorField(transaction);
    var _errorCode = datas[_errorField];
    if(_errorCode !== '00000'){
      Object.keys(PAYBOX_RESPONSE_ERRORS).map(function(error){
        var _regExp = new RegExp(error, 'g');
        if(_regExp.test(_errorCode)){
          _error = PAYBOX_RESPONSE_ERRORS[error].replace(/%ERROR_CODE%/g, _errorCode);
        }
      });
      if(_error === null){
        _error = 'Unknown error returned by paybox : ' + _errorCode;
      }
    }
    return _error;
  },
  getKey            : function(pathOrKey, callback){
    if(pathOrKey.charAt(0) === '/'){
      fs.readFile(pathOrKey, 'utf8', function(err, key){
        callback(key);
      });
    }
    else{
      callback(pathOrKey);
    }
  },
  /**
   * Checks if a query is from Paybox
   * @param  {Object}   transaction   Transaction object returned by paybox.createTransaction()
   * @param  {Object}   datas         Datas received from Paybox
   * @param  {String}   pubkeyPath    The path where Paybox public key is stored on the server
   * @return {Boolean}
   */
  checkIdentity     : function(transaction, datas, pubkeyPath, callback){
    paybox.getKey(pubkeyPath, function(pubkey){
      /*console.log(pubkey);*/
      /*var pubkey     = fs.readFileSync(path.resolve(pubkeyPath), 'utf8');*/
      var _signField  = paybox.getSignField(transaction);
      var _check = true;
      if(_signField !== null){
        var _sign = datas[_signField];
        var _message = [];
        Object.keys(datas).map(function(field){
          if(field !== _signField){
            _message.push(field + '=' + encodeURIComponent(datas[field]));
          }
        });
        _message = _message.join('&');
        _check = paybox.checkSignature(_message, _sign, pubkey);
      }
      callback(_check);
    });
  },
  /**
   * Checks if a message and its signature match with the given public key
   * @param  {String} message   The message with format "field1=val1&field2=val2..."
   * @param  {String} signature The RSA SHA1 signature of the message
   * @param  {String} pubkey    The public key in UTF8
   * @return {Boolean}          If the message is signed by the owner of the public key
   */
  checkSignature    : function(message, signature, pubkey){
    signature = new Buffer(decodeURIComponent(signature), 'base64');
    var check = crypto.createVerify('SHA1');
    check.update(message);
    return check.verify(pubkey, signature);
  },
  /**
   * Generates a signature and inserts it
   * @param  {Object} transaction The transaction returned by paybox.createTransaction()
   * @param  {String} key         Your private key in HEX format
   * @return {Object}             The transaction signed
   */
  signTransaction   : function(transaction, key, callback){
    var error = null;
    transaction['PBX_'].TIME = (new Date()).toISOString();
    transaction['PBX_'].HASH = 'SHA512';
    var _hmac = paybox.generateHMAC(transaction['PBX_'], key);
    transaction['PBX_'].HMAC = _hmac;
    if(_hmac === null){
      error = 'Bad private key format, unable to generate signature';
    }
    callback(error, transaction);
  },
  /**
   * Generates all inputs of the form to submit to Paybox and inserts them in the transaction
   * @param  {Object} transaction The transaction returned by paybox.createTransaction()
   * @return {Object}             The transaction modified
   */
  generateFormBody  : function(transaction){
    transaction.body = Object.keys(transaction['PBX_']).map(function(field){
      return '<input type="hidden" name="PBX_' + field + '" value="' + transaction['PBX_'][field] + '">';
    }).join('');
    return transaction;
  },
  /**
   * Generate an HMAC signature for given PBX fields
   * @param  {Object} PBXFields Paybox parameters without 'PBX_' prefix
   * @param  {String} key       The HEX format of the private key
   * @return {String}           Uppercase HEX format of the HMAC
   */
  generateHMAC      : function(PBXFields, key){
    var _hmac = null;
    try{
      var _message = Object.keys(PBXFields).map(function(field){
        return 'PBX_' + field + '=' + PBXFields[field];
      }).join('&');
      _hmac = paybox.generateSignature(_message, key);
    }
    catch(e){}
    return _hmac;
  },
  generateSignature : function(message, key){
    var _signature = null;
    try{
      var _key = (new Buffer(key, 'hex'));
      _signature = crypto.createHmac('sha512', _key).update(message).digest('hex').toUpperCase();
    }
    catch(e){}
    return _signature;
  },
  /**
   * Finds the field name for the signature in the transaction
   * @param  {Ovject}         transaction   The transaction returned by paybox.createTransaction()
   * @return {String | null}                The field name or NULL if the signature was not asked in the transaction
   */
  getSignField      : function(transaction){
    return paybox.getFieldName(transaction, 'K');
  },
  getErrorField     : function(transaction){
    return paybox.getFieldName(transaction, 'E');
  },
  getFieldName      : function(transaction, code){
    var _pbxRetour = transaction['PBX_'].RETOUR;
    var _fieldsRegExp = new RegExp('^.*;([^;]+):' + code.toUpperCase() + '[;]?.*', 'g');
    var _hasField = _fieldsRegExp.test(_pbxRetour);
    return _hasField ? _pbxRetour.replace(_fieldsRegExp, '$1') : null;
  },
  /**
   * Creates a transaction object with all needed informations to query paybox server
   * @param  {Object}   options  Options to create the transaction
   * @param  {Function} callback Function to be called when the transaction will be created. Arguments are (error, transaction)
   * @return {void}
   */
  createTransaction : function(options, callback){
    paybox.getURL(options.offer, options.isTest === true, function(err, url){
      if(err !== null){
        return callback(err, null);
      }
      var _transaction = {
        'url'         : url,
        'expectedIP'  : '',
        'method'      : options.method || 'GET',
        'body'        : '',
        'PBX_'      : {
          'RUF1'        : 'POST'
        }
      };

      Object.keys(options['PBX_']).map(function(field){
        _transaction['PBX_'][field.toUpperCase()] = options['PBX_'][field];
      });

      paybox.signTransaction(_transaction, options.key, function(error, transaction){
        if(error === null){
          transaction = paybox.generateFormBody(transaction);
        }
        callback(error, transaction);
      });
    });
  },
  /**
   * Extracts informations from an URL formated in a string
   * @param  {String} serverURL The URL with all informations like port, path, host, protocol (only http or https) (ex : https://domain.com:3021/my/path)
   * @return {Object}           An object with all informations extracted from the string
   */
  extractURLInfos   : function(serverURL){
    var _infos = {
      isSSL : false,
      port  : 80,
      path  : '/load.html',
      host  : ''
    };
    _infos.host = serverURL
      .replace(/^(https?):\/\//g, function(p, protocol){
        _infos.isSSL  = protocol === 'https';
        _infos.port   = 443;
        return '';
      })
      .replace(/([^:|^\/]+)(.*)$/g, function(p, host, portAndPath){
        portAndPath
          .replace(/^:(\d+)/g, function(p, port){
            _infos.port = parseInt(port, 10);
            return '';
          })
          .replace(/^:?(\/.*)/g, function(p, path){
            _infos.path = path;
            return '';
          });
        return host;
      });
    return _infos;
  },
  /**
   * Recursive function to check all servers URL given
   * @param  {Array}    servers  List of URLs to check
   * @param  {Integer}  index    Current server index to check in servers Array
   * @param  {Function} callback Function to be called when checks are finished. 2 arguments : err, serverURL
   * @return {void}
   */
  checkNextServer   : function(servers, index, callback){
    var _serverURL = servers[index];
    paybox.checkServer(_serverURL, function(isAlive){
      if(isAlive){
        callback(null, _serverURL);
      }
      else if(++index < servers.length){
        paybox.checkNextServer(servers, index, callback);
      }
      else{
        callback('No alive server found');
      }
    });
  },
  /**
   * Makes a request to the given URL and check for a div#server_status and its content to be OK
   * @param  {String}   serverURL The URL with all needed parameters like port, path, host, protocol (only http or https)
   * @param  {Function} callback  Function to be called when the check is finished with 1 argument : Boolean isAlive
   * @return {void}
   */
  checkServer       : function(serverURL, callback){
    if(serverURL === undefined){
      return callback(false);
    }
    var _server     = paybox.extractURLInfos(serverURL);
    var reqLibrary  = _server.isSSL ? https : http;
    var req = reqLibrary.request(_server);
    req.on('response', function(res){
      var _isAlive = false;
      res.setEncoding('utf8');
      res.on('data', function(body){
        body = body.replace(/< *br *\/? *>/g, '').replace(/\n| /g, '');
        _isAlive = (/id="server_status"/g).test(body) && (/>OK<\/div>/g).test(body);
      });
      res.on('end', function(){
        callback(_isAlive);
      });
    });
    req.on('error', function(err){
      callback(false);
    });
    req.end();
  },
  /**
   * Checks every server for given offer and return a valid one to the callback
   * @param  {String}             offer     The offer we want server URL
   * @param  {optional Boolean}   isTest    If it has to return a test URL
   * @param  {Function}           callback  Function to be called when checks are finished. 2 arguments : err, serverURL
   * @return {void}
   */
  getURL            : function(offer, isTest, callback){
    if(callback === undefined){
      callback  = isTest;
      isTest    = false;
    }
    var _payboxSystemPath = '/cgi/MYchoix_pagepaiement.cgi';
    var servers = paybox.servers(offer, isTest);
    paybox.checkNextServer(servers, 0, function(err, serverURL){
      if(serverURL !== undefined){
        serverURL += _payboxSystemPath;
      }
      callback(err, serverURL);
    });
  },
  /**
   * Returns array of URLs for given offer
   * @param  {String}             offer   The offer the transaction is for. Eg "system"
   * @param  {optional Boolean}   test    If URLs needed are test servers. Default false.
   * @return {Array}                      List of servers URLs
   */
  servers           : function(offer, test){
    var _serversURLs = [];
    if(PAYBOX_SERVERS[offer] !== undefined){
      _serversURLs = PAYBOX_SERVERS[offer][test ? 'test' : 'prod'];
    }
    return _serversURLs;
  }
};

module.exports = paybox;
<|endoftext|>