<commit_before>
/*!
 * CSS - Lexer
 * Copyright(c) 2010 LearnBoost <dev@learnboost.com>
 * MIT Licensed
 */

/**
 * Module dependencies.
 */

var Token = require('./token')
  , nodes = require('./nodes');

/**
 * Operator aliases.
 */

var alias = {
    and: '&&'
  , or: '||'
};

/**
 * Numeric strings used for exceptions.
 */

var numberString = [, 'one', 'two'];

/**
 * Units.
 */

var units = [
    'em'
  , 'ex'
  , 'px'
  , 'mm'
  , 'cm'
  , 'in'
  , 'pt'
  , 'pc'
  , 'deg'
  , 'rad'
  , 'grad'
  , 'ms'
  , 's'
  , 'Hz'
  , 'kHz'
  , '%'].join('|');

/**
 * Unit RegExp.
 */

var unit = new RegExp('^(-)?(\\d+\\.\\d+|\\d+|\\.\\d+)(' + units + ')? *');

/**
 * Initialize a new `Lexer` with the given `str` and `options`.
 *
 * @param {String} str
 * @param {Object} options
 * @api private
 */

var Lexer = module.exports = function Lexer(str, options) {
  options = options || {};
  this.str = str.replace(/\r\n?/g, '\n').replace(/\t/g, '  ');
  this.stash = [];
  this.prevIndents = 0;
  this.lineno = 0;
};

/**
 * Lexer prototype.
 */

Lexer.prototype = {

  /**
   * Custom inspect.
   */

  inspect: function(){
    var tok
      , tmp = this.str
      , buf = [];
    while ('eos' != (tok = this.next).type) {
      buf.push(tok.inspect());
    }
    this.str = tmp;
    this.prevIndents = 0;
    return buf.concat(tok.inspect()).join('\n');
  },

  /**
   * Lookahead `n` tokens.
   *
   * @param {Number} n
   * @return {Object}
   * @api private
   */

  lookahead: function(n){
    var fetch = n - this.stash.length;
    while (fetch-- > 0) this.stash.push(this.advance);
    return this.stash[--n];
  },

  /**
   * Consume the given `len`.
   *
   * @param {Number|Array} len
   * @api private
   */

  skip: function(len){
    this.str = this.str.substr(Array.isArray(len)
      ? len[0].length
      : len);
  },

  /**
   * Fetch next token including those stashed by peek.
   *
   * @return {Token}
   * @api private
   */

  get next() {
    var tok = this.stashed || this.advance;
    switch (tok.type) {
      case 'newline':
      case 'selector':
      case 'indent':
        ++this.lineno;
    }
    tok.lineno = this.lineno;
    return tok;
  },

  /**
   * Fetch next token.
   *
   * @return {Token}
   * @api private
   */

  get advance() {
    return this.eos
      || this.null
      || this.keyword
      || this.atrule
      || this.media
      || this.comment
      || this.newline
      || this.escaped
      || this.important
      || this.literal
      || this.function
      || this.brace
      || this.paren
      || this.color
      || this.string
      || this.unit
      || this.namedop
      || this.boolean
      || this.ident
      || this.op
      || this.space
      || this.selector;
  },

  /**
   * Lookahead a single token.
   *
   * @return {Token}
   * @api private
   */

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

  /**
   * Return the next possibly stashed token.
   *
   * @return {Token}
   * @api private
   */

  get stashed() {
    return this.stash.shift();
  },

  /**
   * EOS | trailing outdents.
   */

  get eos() {
    if (this.str.length) return;
    return --this.prevIndents < 0
      ? new Token('eos')
      : new Token('outdent');
  },

  /**
   * ' '+
   */

  get space() {
    var captures;
    if (captures = /^( *)/.exec(this.str)) {
      this.skip(captures);
      return new Token('space');
    }
  },

  /**
   * '\\' . ' '*
   */

  get escaped() {
    var captures;
    if (captures = /^\\(.) */.exec(this.str)) {
      var c = captures[1];
      this.skip(captures);
      return new Token('ident', new nodes.Ident(c));
    }
  },

  /**
   * '@css' ' '* '{' .* '}' ' '*
   */

  get literal() {
    // HACK attack !!!
    var captures;
    if (captures = /^@css *\{/.exec(this.str)) {
      this.skip(captures);
      var c
        , braces = 1
        , css = '';
      while (c = this.str[0]) {
        this.str = this.str.substr(1);
        switch (c) {
          case '{': ++braces; break;
          case '}': --braces; break;
        }
        css += c;
        if (!braces) break;
      }
      css = css.replace(/\s*}$/, '');
      return new Token('literal', new nodes.Literal(css));
    }
  },

  /**
   * '!important' ' '*
   */

  get important() {
    var captures;
    if (captures = /^!important */.exec(this.str)) {
      this.skip(captures);
      return new Token('ident', new nodes.Ident('!important'));
    }
  },

  /**
   * '{' | '}'
   */

  get brace() {
    var captures;
    if (captures = /^([{}])/.exec(this.str)) {
      this.skip(1);
      var brace = captures[1];
      return new Token(brace, brace);
    }
  },

  /**
   * '(' | ')' ' '*
   */

  get paren() {
    var captures;
    if (captures = /^([()]) */.exec(this.str)) {
      var paren = captures[1];
      this.skip(captures);
      if (')' == paren) this.isURL = false;
      return new Token(paren, paren);
    }
  },

  /**
   * 'null'
   */

  get null() {
    var captures;
    if (captures = /^(null)\b */.exec(this.str)) {
      this.skip(captures);
      return new Token('null', nodes.null);
    }
  },

  /**
   *   'if'
   * | 'else'
   * | 'unless'
   * | 'return'
   * | 'for'
   * | 'in'
   */

  get keyword() {
    var captures;
    if (captures = /^(return|if|else|unless|for|in)\b */.exec(this.str)) {
      var keyword = captures[1];
      this.skip(captures);
      return new Token(keyword, keyword);
    }
  },

  /**
   *   'not'
   * | 'and'
   * | 'or'
   * | 'is a'
   * | 'is defined'
   */

  get namedop() {
    var captures;
    if (captures = /^(not|and|or|is a|is defined)\b( *)/.exec(this.str)) {
      var op = captures[1];
      this.skip(captures);
      op = alias[op] || op;
      var tok = new Token(op, op);
      tok.space = captures[2];
      return tok;
    }
  },

  /**
   *   ','
   * | '+'
   * | '-'
   * | '*'
   * | '/'
   * | '%'
   * | '**'
   * | '!'
   * | '&'
   * | '&&'
   * | '||'
   * | '>'
   * | '>='
   * | '<'
   * | '<='
   * | '='
   * | '=='
   * | '!='
   * | '!'
   * | '~'
   * | '?='
   * | '?'
   * | ':'
   * | '['
   * | ']'
   * | '..'
   * | '...'
   */

  get op() {
    var captures;
    if (captures = /^([.]{2,3}|&&|\|\||[!<>=?]=|\*\*|[-,=?:!~+<>*\/%&\[\]])( *)/.exec(this.str)) {
      var op = captures[1];
      this.skip(captures);
      op = alias[op] || op;
      var tok = new Token(op, op);
      tok.space = captures[2];
      return tok;
    }
  },

  /**
   * '@media' ([^\n]+)
   */

  get media() {
    var captures;
    if (captures = /^@media *([^\n]+)/.exec(this.str)) {
      this.skip(captures);
      return new Token('media', captures[1]);
    }
  },

  /**
   * '@' ('import' | 'keyframes' | 'charset')
   */

  get atrule() {
    var captures;
    if (captures = /^@(import|keyframes|charset) */.exec(this.str)) {
      this.skip(captures);
      return new Token(captures[1]);
    }
  },

  /**
   * '//' *
   */

  get comment() {
    // Single line
    if ('/' == this.str[0] && '/' == this.str[1]) {
      var end = this.str.indexOf('\n');
      if (-1 == end) end = this.str.length;
      this.skip(end);
      return this.advance;
    }

    // Multi-line
    if ('/' == this.str[0] && '*' == this.str[1]) {
      var end = this.str.indexOf('*/');
      if (-1 == end) end = this.str.length;
      var str = this.str.substr(0, end + 2)
        , lines = str.split('\n').length - 1;
      this.lineno += lines;
      this.skip(end + 2);
      return this.allowComments
        ? new Token('comment', str)
        : this.advance;
    }
  },

  /**
   * 'true' | 'false'
   */

  get boolean() {
    var captures;
    if (captures = /^(true|false)\b( *)/.exec(this.str)) {
      var val = 'true' == captures[1]
        ? nodes.true
        : nodes.false;
      this.skip(captures);
      var tok = new Token('boolean', val);
      tok.space = captures[2];
      return tok;
    }
  },

  /**
   * -?[a-zA-Z] [-\w\d]* '('
   */

  get function() {
    var captures;
    if (captures = /^(-?[a-zA-Z][-\w\d]*)\(( *)/.exec(this.str)) {
      var name = captures[1];
      this.skip(captures);
      this.isURL = 'url' == name;
      var tok = new Token('function', new nodes.Ident(name));
      tok.space = captures[2];
      return tok;
    }
  },

  /**
   * -?[a-zA-Z] [-\w\d]*
   */

  get ident() {
    var captures;
    if (captures = /^(-?[a-zA-Z][-\w\d]*)( *)/.exec(this.str)) {
      var name = captures[1];
      this.skip(captures);
      var tok = new Token('ident', new nodes.Ident(name));
      tok.space = captures[2];
      return tok;
    }
  },

  /**
   * '\n' ' '+
   */

  get newline() {
    var captures;
    if (captures = /^\n( *)/.exec(this.str)) {
      var tok
        , spaces = captures[1].length
        , indents = spaces / 2;

      this.skip(captures);

      // Reset state
      this.isVariable = false;

      // Blank line
      if ('\n' == this.str[0]) {
        ++this.lineno;
        return this.advance;
      }

      // To few spaces
      if (0 != spaces % 2) {
        var str = 1 == spaces ? 'space' : 'spaces'
          , spaces = numberString[spaces] || spaces;
        throw new Error('Invalid indentation, got ' + spaces + ' ' + str + ' and expected multiple of two');
      // To many spaces
      } else if (indents > this.prevIndents + 1) {
        var str = 1 == spaces ? 'space' : 'spaces'
          , expected = 2 * (this.prevIndents * 2) || 2
          , expected = numberString[expected] || expected
          , spaces = numberString[spaces] || spaces;
        throw new Error('Invalid indentation, got ' + spaces + ' ' + str + ' and expected ' + expected);
      // Outdent
      } else if (indents < this.prevIndents) {
        var n = this.prevIndents - indents;
        while (--n) this.stash.push(new Token('outdent'));
        this.prevIndents = indents;
        tok = new Token('outdent');
      // Indent
      } else if (indents != this.prevIndents) {
        this.prevIndents = indents;
        tok = new Token('indent');
      // Newline
      } else {
        tok = new Token('newline');
      }

      return tok;
    }
  },

  /**
   * '-'? (digit+ | digit* '.' digit+) unit
   */

  get unit() {
    var captures;
    if (captures = unit.exec(this.str)) {
      this.skip(captures);
      var n = parseFloat(captures[2]);
      if ('-' == captures[1]) n = -n;
      var node = new nodes.Unit(n, captures[3]);
      return new Token('unit', node);
    }
  },

  /**
   * '"' [^"]+ '"' | "'"" [^']+ "'"
   */

  get string() {
    var captures;

    // url() special case
    if (this.isURL && (captures = /^([^'"][^)]+) */.exec(this.str))) {
      this.skip(captures);
      return new Token('string', new nodes.String(captures[1]));
    }

    // Regular string
    if (captures = /^("[^"]*"|'[^']*') */.exec(this.str)) {
      var str = captures[1];
      this.skip(captures);
      return new Token('string', new nodes.String(str.slice(1,-1)));
    }
  },

  /**
   * #nnnnnn | #nnn
   */

  get color() {
    return this.hex6 || this.hex3;
  },

  /**
   * #nnn
   */

  get hex3() {
    var captures;
    if (captures = /^#([a-f-A-F0-9]{3}) */.exec(this.str)) {
      this.skip(captures);
      var rgb = captures[1]
        , r = parseInt(rgb[0] + rgb[0], 16)
        , g = parseInt(rgb[1] + rgb[1], 16)
        , b = parseInt(rgb[2] + rgb[2], 16);
      return new Token('color', new nodes.Color(r, g, b, 1));
    }
  },

  /**
   * #nnnnnn
   */

  get hex6() {
    var captures;
    if (captures = /^#([a-f-A-F0-9]{6}) */.exec(this.str)) {
      this.skip(captures);
      var rgb = captures[1]
        , r = parseInt(rgb.substr(0, 2), 16)
        , g = parseInt(rgb.substr(2, 2), 16)
        , b = parseInt(rgb.substr(4, 2), 16);
      return new Token('color', new nodes.Color(r, g, b, 1));
    }
  },

  /**
   * [^\n,;]+
   */

  get selector() {
    var captures;
    if (captures = /^[^\n,]+/.exec(this.str)) {
      var selector = captures[0];
      this.skip(captures);
      return new Token('selector', selector);
    }
  }
};
<commit_msg>Parser#ident no longer consuming trailing whitespace<commit_after>
/*!
 * CSS - Lexer
 * Copyright(c) 2010 LearnBoost <dev@learnboost.com>
 * MIT Licensed
 */

/**
 * Module dependencies.
 */

var Token = require('./token')
  , nodes = require('./nodes');

/**
 * Operator aliases.
 */

var alias = {
    and: '&&'
  , or: '||'
};

/**
 * Numeric strings used for exceptions.
 */

var numberString = [, 'one', 'two'];

/**
 * Units.
 */

var units = [
    'em'
  , 'ex'
  , 'px'
  , 'mm'
  , 'cm'
  , 'in'
  , 'pt'
  , 'pc'
  , 'deg'
  , 'rad'
  , 'grad'
  , 'ms'
  , 's'
  , 'Hz'
  , 'kHz'
  , '%'].join('|');

/**
 * Unit RegExp.
 */

var unit = new RegExp('^(-)?(\\d+\\.\\d+|\\d+|\\.\\d+)(' + units + ')? *');

/**
 * Initialize a new `Lexer` with the given `str` and `options`.
 *
 * @param {String} str
 * @param {Object} options
 * @api private
 */

var Lexer = module.exports = function Lexer(str, options) {
  options = options || {};
  this.str = str.replace(/\r\n?/g, '\n').replace(/\t/g, '  ');
  this.stash = [];
  this.prevIndents = 0;
  this.lineno = 0;
};

/**
 * Lexer prototype.
 */

Lexer.prototype = {

  /**
   * Custom inspect.
   */

  inspect: function(){
    var tok
      , tmp = this.str
      , buf = [];
    while ('eos' != (tok = this.next).type) {
      buf.push(tok.inspect());
    }
    this.str = tmp;
    this.prevIndents = 0;
    return buf.concat(tok.inspect()).join('\n');
  },

  /**
   * Lookahead `n` tokens.
   *
   * @param {Number} n
   * @return {Object}
   * @api private
   */

  lookahead: function(n){
    var fetch = n - this.stash.length;
    while (fetch-- > 0) this.stash.push(this.advance);
    return this.stash[--n];
  },

  /**
   * Consume the given `len`.
   *
   * @param {Number|Array} len
   * @api private
   */

  skip: function(len){
    this.str = this.str.substr(Array.isArray(len)
      ? len[0].length
      : len);
  },

  /**
   * Fetch next token including those stashed by peek.
   *
   * @return {Token}
   * @api private
   */

  get next() {
    var tok = this.stashed || this.advance;
    switch (tok.type) {
      case 'newline':
      case 'selector':
      case 'indent':
        ++this.lineno;
    }
    tok.lineno = this.lineno;
    return tok;
  },

  /**
   * Fetch next token.
   *
   * @return {Token}
   * @api private
   */

  get advance() {
    return this.eos
      || this.null
      || this.keyword
      || this.atrule
      || this.media
      || this.comment
      || this.newline
      || this.escaped
      || this.important
      || this.literal
      || this.function
      || this.brace
      || this.paren
      || this.color
      || this.string
      || this.unit
      || this.namedop
      || this.boolean
      || this.ident
      || this.op
      || this.space
      || this.selector;
  },

  /**
   * Lookahead a single token.
   *
   * @return {Token}
   * @api private
   */

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

  /**
   * Return the next possibly stashed token.
   *
   * @return {Token}
   * @api private
   */

  get stashed() {
    return this.stash.shift();
  },

  /**
   * EOS | trailing outdents.
   */

  get eos() {
    if (this.str.length) return;
    return --this.prevIndents < 0
      ? new Token('eos')
      : new Token('outdent');
  },

  /**
   * ' '+
   */

  get space() {
    var captures;
    if (captures = /^( *)/.exec(this.str)) {
      this.skip(captures);
      return new Token('space');
    }
  },

  /**
   * '\\' . ' '*
   */

  get escaped() {
    var captures;
    if (captures = /^\\(.) */.exec(this.str)) {
      var c = captures[1];
      this.skip(captures);
      return new Token('ident', new nodes.Ident(c));
    }
  },

  /**
   * '@css' ' '* '{' .* '}' ' '*
   */

  get literal() {
    // HACK attack !!!
    var captures;
    if (captures = /^@css *\{/.exec(this.str)) {
      this.skip(captures);
      var c
        , braces = 1
        , css = '';
      while (c = this.str[0]) {
        this.str = this.str.substr(1);
        switch (c) {
          case '{': ++braces; break;
          case '}': --braces; break;
        }
        css += c;
        if (!braces) break;
      }
      css = css.replace(/\s*}$/, '');
      return new Token('literal', new nodes.Literal(css));
    }
  },

  /**
   * '!important' ' '*
   */

  get important() {
    var captures;
    if (captures = /^!important */.exec(this.str)) {
      this.skip(captures);
      return new Token('ident', new nodes.Ident('!important'));
    }
  },

  /**
   * '{' | '}'
   */

  get brace() {
    var captures;
    if (captures = /^([{}])/.exec(this.str)) {
      this.skip(1);
      var brace = captures[1];
      return new Token(brace, brace);
    }
  },

  /**
   * '(' | ')' ' '*
   */

  get paren() {
    var captures;
    if (captures = /^([()]) */.exec(this.str)) {
      var paren = captures[1];
      this.skip(captures);
      if (')' == paren) this.isURL = false;
      return new Token(paren, paren);
    }
  },

  /**
   * 'null'
   */

  get null() {
    var captures;
    if (captures = /^(null)\b */.exec(this.str)) {
      this.skip(captures);
      return new Token('null', nodes.null);
    }
  },

  /**
   *   'if'
   * | 'else'
   * | 'unless'
   * | 'return'
   * | 'for'
   * | 'in'
   */

  get keyword() {
    var captures;
    if (captures = /^(return|if|else|unless|for|in)\b */.exec(this.str)) {
      var keyword = captures[1];
      this.skip(captures);
      return new Token(keyword, keyword);
    }
  },

  /**
   *   'not'
   * | 'and'
   * | 'or'
   * | 'is a'
   * | 'is defined'
   */

  get namedop() {
    var captures;
    if (captures = /^(not|and|or|is a|is defined)\b( *)/.exec(this.str)) {
      var op = captures[1];
      this.skip(captures);
      op = alias[op] || op;
      var tok = new Token(op, op);
      tok.space = captures[2];
      return tok;
    }
  },

  /**
   *   ','
   * | '+'
   * | '-'
   * | '*'
   * | '/'
   * | '%'
   * | '**'
   * | '!'
   * | '&'
   * | '&&'
   * | '||'
   * | '>'
   * | '>='
   * | '<'
   * | '<='
   * | '='
   * | '=='
   * | '!='
   * | '!'
   * | '~'
   * | '?='
   * | '?'
   * | ':'
   * | '['
   * | ']'
   * | '..'
   * | '...'
   */

  get op() {
    var captures;
    if (captures = /^([.]{2,3}|&&|\|\||[!<>=?]=|\*\*|[-,=?:!~+<>*\/%&\[\]])( *)/.exec(this.str)) {
      var op = captures[1];
      this.skip(captures);
      op = alias[op] || op;
      var tok = new Token(op, op);
      tok.space = captures[2];
      return tok;
    }
  },

  /**
   * '@media' ([^\n]+)
   */

  get media() {
    var captures;
    if (captures = /^@media *([^\n]+)/.exec(this.str)) {
      this.skip(captures);
      return new Token('media', captures[1]);
    }
  },

  /**
   * '@' ('import' | 'keyframes' | 'charset')
   */

  get atrule() {
    var captures;
    if (captures = /^@(import|keyframes|charset) */.exec(this.str)) {
      this.skip(captures);
      return new Token(captures[1]);
    }
  },

  /**
   * '//' *
   */

  get comment() {
    // Single line
    if ('/' == this.str[0] && '/' == this.str[1]) {
      var end = this.str.indexOf('\n');
      if (-1 == end) end = this.str.length;
      this.skip(end);
      return this.advance;
    }

    // Multi-line
    if ('/' == this.str[0] && '*' == this.str[1]) {
      var end = this.str.indexOf('*/');
      if (-1 == end) end = this.str.length;
      var str = this.str.substr(0, end + 2)
        , lines = str.split('\n').length - 1;
      this.lineno += lines;
      this.skip(end + 2);
      return this.allowComments
        ? new Token('comment', str)
        : this.advance;
    }
  },

  /**
   * 'true' | 'false'
   */

  get boolean() {
    var captures;
    if (captures = /^(true|false)\b( *)/.exec(this.str)) {
      var val = 'true' == captures[1]
        ? nodes.true
        : nodes.false;
      this.skip(captures);
      var tok = new Token('boolean', val);
      tok.space = captures[2];
      return tok;
    }
  },

  /**
   * -?[a-zA-Z] [-\w\d]* '('
   */

  get function() {
    var captures;
    if (captures = /^(-?[a-zA-Z][-\w\d]*)\(( *)/.exec(this.str)) {
      var name = captures[1];
      this.skip(captures);
      this.isURL = 'url' == name;
      var tok = new Token('function', new nodes.Ident(name));
      tok.space = captures[2];
      return tok;
    }
  },

  /**
   * -?[a-zA-Z] [-\w\d]*
   */

  get ident() {
    var captures;
    if (captures = /^(-?[a-zA-Z][-\w\d]*)/.exec(this.str)) {
      var name = captures[1];
      this.skip(captures);
      return new Token('ident', new nodes.Ident(name));
    }
  },

  /**
   * '\n' ' '+
   */

  get newline() {
    var captures;
    if (captures = /^\n( *)/.exec(this.str)) {
      var tok
        , spaces = captures[1].length
        , indents = spaces / 2;

      this.skip(captures);

      // Reset state
      this.isVariable = false;

      // Blank line
      if ('\n' == this.str[0]) {
        ++this.lineno;
        return this.advance;
      }

      // To few spaces
      if (0 != spaces % 2) {
        var str = 1 == spaces ? 'space' : 'spaces'
          , spaces = numberString[spaces] || spaces;
        throw new Error('Invalid indentation, got ' + spaces + ' ' + str + ' and expected multiple of two');
      // To many spaces
      } else if (indents > this.prevIndents + 1) {
        var str = 1 == spaces ? 'space' : 'spaces'
          , expected = 2 * (this.prevIndents * 2) || 2
          , expected = numberString[expected] || expected
          , spaces = numberString[spaces] || spaces;
        throw new Error('Invalid indentation, got ' + spaces + ' ' + str + ' and expected ' + expected);
      // Outdent
      } else if (indents < this.prevIndents) {
        var n = this.prevIndents - indents;
        while (--n) this.stash.push(new Token('outdent'));
        this.prevIndents = indents;
        tok = new Token('outdent');
      // Indent
      } else if (indents != this.prevIndents) {
        this.prevIndents = indents;
        tok = new Token('indent');
      // Newline
      } else {
        tok = new Token('newline');
      }

      return tok;
    }
  },

  /**
   * '-'? (digit+ | digit* '.' digit+) unit
   */

  get unit() {
    var captures;
    if (captures = unit.exec(this.str)) {
      this.skip(captures);
      var n = parseFloat(captures[2]);
      if ('-' == captures[1]) n = -n;
      var node = new nodes.Unit(n, captures[3]);
      return new Token('unit', node);
    }
  },

  /**
   * '"' [^"]+ '"' | "'"" [^']+ "'"
   */

  get string() {
    var captures;

    // url() special case
    if (this.isURL && (captures = /^([^'"][^)]+) */.exec(this.str))) {
      this.skip(captures);
      return new Token('string', new nodes.String(captures[1]));
    }

    // Regular string
    if (captures = /^("[^"]*"|'[^']*') */.exec(this.str)) {
      var str = captures[1];
      this.skip(captures);
      return new Token('string', new nodes.String(str.slice(1,-1)));
    }
  },

  /**
   * #nnnnnn | #nnn
   */

  get color() {
    return this.hex6 || this.hex3;
  },

  /**
   * #nnn
   */

  get hex3() {
    var captures;
    if (captures = /^#([a-f-A-F0-9]{3}) */.exec(this.str)) {
      this.skip(captures);
      var rgb = captures[1]
        , r = parseInt(rgb[0] + rgb[0], 16)
        , g = parseInt(rgb[1] + rgb[1], 16)
        , b = parseInt(rgb[2] + rgb[2], 16);
      return new Token('color', new nodes.Color(r, g, b, 1));
    }
  },

  /**
   * #nnnnnn
   */

  get hex6() {
    var captures;
    if (captures = /^#([a-f-A-F0-9]{6}) */.exec(this.str)) {
      this.skip(captures);
      var rgb = captures[1]
        , r = parseInt(rgb.substr(0, 2), 16)
        , g = parseInt(rgb.substr(2, 2), 16)
        , b = parseInt(rgb.substr(4, 2), 16);
      return new Token('color', new nodes.Color(r, g, b, 1));
    }
  },

  /**
   * [^\n,;]+
   */

  get selector() {
    var captures;
    if (captures = /^[^\n,]+/.exec(this.str)) {
      var selector = captures[0];
      this.skip(captures);
      return new Token('selector', selector);
    }
  }
};
<|endoftext|>


<commit_before>import { homedir } from 'os';
import { join as pathJoin } from 'path';
import fs from 'fs-promise';
import fetch from 'node-fetch';
import { stringify as stringifyQuery } from 'querystring';
import chalk from 'chalk';

const stdin = process.openStdin();

function readEmail () {
  return new Promise((resolve, reject) => {
    process.stdout.write('> Enter your email address: ');
    stdin.on('data', (d) => {
      stdin.destroy();
      resolve(d.toString().trim());
    });
  });
}

async function getVerificationToken (email) {
  const data = JSON.stringify({ email });
  const res = await fetch('http://localhost:3001/', {
    method: 'POST',
    mode: 'cors',
    headers: {
      'Content-Type': 'application/json',
      'Content-Length': data.length
    },
    body: data
  });

  if (200 !== res.status) {
    throw new Error('Verification error');
  }

  const body = await res.json();
  return body.token;
}

async function verify (email, verificationToken) {
  const query = {
    email,
    token: verificationToken
  };

  const res = await fetch('http://localhost:3001/verify?' + stringifyQuery(query));

  const body = await res.json();
  return body.token;
}

function sleep (ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms);
  });
}

async function register () {
  const email = await readEmail();
  const verificationToken = await getVerificationToken(email);

  console.log(`> Please follow the link sent to ${chalk.bold(email)} to log in.`);
  process.stdout.write('> Waiting for confirmation..');

  let final;
  do {
    await sleep(2500);
    try {
      final = await verify(email, verificationToken);
    } catch (e) {}
    process.stdout.write('.');
  } while (!final);

  process.stdout.write('\n');

  return { email, token: final };
}

export default function () {
  return register().then((loginData) => {
    return fs.writeFile(pathJoin(homedir(), '.now.json'), JSON.stringify(loginData));
  });
}
<commit_msg>login: tweak requests<commit_after>import { homedir } from 'os';
import { join as pathJoin } from 'path';
import fs from 'fs-promise';
import fetch from 'node-fetch';
import { stringify as stringifyQuery } from 'querystring';
import chalk from 'chalk';

const URL = 'https://api.now.sh/registration';

const stdin = process.openStdin();

function readEmail () {
  return new Promise((resolve, reject) => {
    process.stdout.write('> Enter your email address: ');
    stdin.on('data', (d) => {
      stdin.destroy();
      resolve(d.toString().trim());
    });
  });
}

async function getVerificationToken (email) {
  const data = JSON.stringify({ email });
  const res = await fetch(URL, {
    method: 'POST',
    body: data
  });

  if (200 !== res.status) {
    throw new Error('Verification error');
  }

  const body = await res.json();
  return body.token;
}

async function verify (email, verificationToken) {
  const query = {
    email,
    token: verificationToken
  };

  const res = await fetch(`${URL}/verify?${stringifyQuery(query)}`);
  const body = await res.json();
  return body.token;
}

function sleep (ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms);
  });
}

async function register () {
  const email = await readEmail();
  const verificationToken = await getVerificationToken(email);

  console.log(`> Please follow the link sent to ${chalk.bold(email)} to log in.`);
  process.stdout.write('> Waiting for confirmation..');

  let final;
  do {
    await sleep(2500);
    try {
      final = await verify(email, verificationToken);
    } catch (e) {}
    process.stdout.write('.');
  } while (!final);

  process.stdout.write('\n');

  return { email, token: final };
}

export default function () {
  return register().then((loginData) => {
    return fs.writeFile(pathJoin(homedir(), '.now.json'), JSON.stringify(loginData));
  });
}
<|endoftext|>


<commit_before>/* eslint-env node */
module.exports = {
  "framework": "qunit",
  "parallel": 5,
  "disable_watching": true,
  "timeout": 600,
  "browser_start_timeout": 90,
  "test_page": "tests/index.html?hidepassed",
  "on_start": "./sauce_labs/saucie-connect.js",
  "on_exit": "./sauce_labs/saucie-disconnect.js",
  "port": 8080,
  "launchers": {
    "SL_Chrome_Current": {
      "exe": "saucie",
      "args": ["-b", "chrome", "-p", "Windows 10", "-v", "latest", "--no-connect", "-u"],
      "protocol": "tap"
    },
    "SL_MS_Edge": {
      "exe": "saucie",
      "args": ["-b", "microsoftedge", "-v", "15", "--no-connect", "-u"],
      "protocol": "tap"
    },
    "SL_IE_11": {
      "exe": "saucie",
      "args": ["-b", "internet explorer", "-v", "11", "--no-connect", "-u"],
      "protocol": "tap"
    },
    "SL_Safari_Current": {
      "exe": "saucie",
      "args": ["-b", "safari", "-v", "9", "--no-connect", "-u"],
      "protocol": "tap"
    }
  },
  "launch_in_ci": [
    "Chrome",
    "Firefox",
    "SL_Safari_Current",
    "SL_MS_Edge",
    "SL_IE_11"
  ],
  "browser_args": {
    "Chrome": [
      "--no-sandbox", // Fixes issue starting ChromeHeadless, see https://github.com/travis-ci/travis-ci/issues/9024
      "--disable-gpu",
      "--headless",
      "--remote-debugging-port=9222",
      "--window-size=1440,900"
    ]
  }
};
<commit_msg>Use Safari latest<commit_after>/* eslint-env node */
module.exports = {
  "framework": "qunit",
  "parallel": 5,
  "disable_watching": true,
  "timeout": 600,
  "browser_start_timeout": 90,
  "test_page": "tests/index.html?hidepassed",
  "on_start": "./sauce_labs/saucie-connect.js",
  "on_exit": "./sauce_labs/saucie-disconnect.js",
  "port": 8080,
  "launchers": {
    "SL_Chrome_Current": {
      "exe": "saucie",
      "args": ["-b", "chrome", "-p", "Windows 10", "-v", "latest", "--no-connect", "-u"],
      "protocol": "tap"
    },
    "SL_MS_Edge": {
      "exe": "saucie",
      "args": ["-b", "microsoftedge", "-v", "15", "--no-connect", "-u"],
      "protocol": "tap"
    },
    "SL_IE_11": {
      "exe": "saucie",
      "args": ["-b", "internet explorer", "-v", "11", "--no-connect", "-u"],
      "protocol": "tap"
    },
    "SL_Safari_Current": {
      "exe": "saucie",
      "args": ["-b", "safari", "-v", "latest", "--no-connect", "-u"],
      "protocol": "tap"
    }
  },
  "launch_in_ci": [
    "Chrome",
    "Firefox",
    "SL_Safari_Current",
    "SL_MS_Edge",
    "SL_IE_11"
  ],
  "browser_args": {
    "Chrome": [
      "--no-sandbox", // Fixes issue starting ChromeHeadless, see https://github.com/travis-ci/travis-ci/issues/9024
      "--disable-gpu",
      "--headless",
      "--remote-debugging-port=9222",
      "--window-size=1440,900"
    ]
  }
};
<|endoftext|>


<commit_before>var chai = require('chai');
var collections = require('../lib/collections');
var spies = require('chai-spies');

chai.use(spies);
var expect = chai.expect;

var list = [];
for (var i = 0; i < 1000; i++) {
  list.push(i);
};
var err = new Error();

describe('collections', function() {
  describe('#forEach', function() {
    it('should have forEach', function() {
      expect(collections).to.have.property('forEach');
    });

    it('should be fulfilled with the number of elements', function(done) {
      var spy = chai.spy(function() {});
      var iterator = collections.forEach(list, spy);

      iterator.then(function(value) {
        expect(value).to.eql(list.length);
        expect(spy).to.have.been.called.exactly(list.length);
        done();
      });
    });

    it('should reject if the iterator throws an error', function(done) {
      var iterator = collections.forEach(list, function() {
        throw (err);
      });

      iterator.then(null, function(reason) {
        expect(reason).to.eql(err);
        done();
      });
    });
  });

  describe('#every', function() {
    it('should have every', function() {
      expect(collections).to.have.property('every');
    });

    it('should return true if all match', function(done) {
      var predicate = function(num) {
        return num >= 0;
      };
      var every = collections.every(list, predicate);

      every.then(function(value) {
        expect(value).to.eql(list.every(predicate));
        done();
      });
    });

    it('should return false if some do not match', function(done) {
      var predicate = function(num) {
        return num > 0;
      };
      var every = collections.every(list, predicate);

      every.then(function(value) {
        expect(value).to.eql(list.every(predicate));
        done();
      });
    });
  });

  describe('#some', function() {
    it('should have some', function() {
      expect(collections).to.have.property('some');
    });

    it('should return true if any match', function(done) {
      var predicate = function(num) {
        return num === 1;
      }
      var some = collections.some(list, predicate);

      some.then(function(value) {
        expect(value).to.eql(list.some(predicate));
        done();
      });
    });

    it('should return false if none match', function(done) {
      var predicate = function(num) {
        return num === -1;
      }
      var some = collections.some(list, predicate);

      some.then(function(value) {
        expect(value).to.eql(list.some(predicate));
        done();
      });
    });
  });

  describe('#filter', function() {
    it('should have filter', function() {
      expect(collections).to.have.property('filter');
    });

    it('should filter the elements', function(done) {
      var predicate = function(element) {
        return element > 1;
      }
      var filtered = collections.filter(list, predicate);

      filtered.then(function(value) {
        expect(value).to.eql(list.filter(predicate));
        done();
      });
    });
  });

  describe('#map', function() {
    it('should have map', function() {
      expect(collections).to.have.property('map');
    });

    it('should apply to all the elements', function(done) {
      var transform = function(element) {
        return element * 2;
      };
      var mapped = collections.map(list, transform);

      mapped.then(function(value) {
        expect(value).to.eql(list.map(transform));
        done();
      });
    });
  });

  describe('#reverse', function() {
    it('should have reverse', function() {
      expect(collections).to.have.property('reverse');
    });

    it('should apply to all the elements', function(done) {
      var reversed = collections.reverse(list);

      reversed.then(function(value) {
        expect(value).to.eql(list.reverse());
        done();
      });
    });
  });

  describe('#reduce', function() {
    it('should have reduce', function() {
      expect(collections).to.have.property('reduce');
    });

    it('should apply to all the elements', function(done) {
      var transform = function(previous, element) {
        return element * 2;
      };
      var reduced = collections.reduce(list, transform);

      reduced.then(function(value) {
        expect(value).to.eql(list.reduce(transform));
        done();
      });
    });
  });

  describe('#reduceRight', function() {
    it('should have reduceRight', function() {
      expect(collections).to.have.property('reduceRight');
    });

    it('should apply to all the elements', function(done) {
      var transform = function(previous, element) {
        return element * 2;
      };
      var reduced = collections.reduceRight(list, transform);

      reduced.then(function(value) {
        expect(value).to.eql(list.reduceRight(transform));
        done();
      });
    });
  });
});<commit_msg>Fixing semicolons in collection_spec.js<commit_after>var chai = require('chai');
var collections = require('../lib/collections');
var spies = require('chai-spies');

chai.use(spies);
var expect = chai.expect;

var list = [];
for (var i = 0; i < 1000; i++) {
  list.push(i);
}
var err = new Error();

describe('collections', function() {
  describe('#forEach', function() {
    it('should have forEach', function() {
      expect(collections).to.have.property('forEach');
    });

    it('should be fulfilled with the number of elements', function(done) {
      var spy = chai.spy(function() {});
      var iterator = collections.forEach(list, spy);

      iterator.then(function(value) {
        expect(value).to.eql(list.length);
        expect(spy).to.have.been.called.exactly(list.length);
        done();
      });
    });

    it('should reject if the iterator throws an error', function(done) {
      var iterator = collections.forEach(list, function() {
        throw (err);
      });

      iterator.then(null, function(reason) {
        expect(reason).to.eql(err);
        done();
      });
    });
  });

  describe('#every', function() {
    it('should have every', function() {
      expect(collections).to.have.property('every');
    });

    it('should return true if all match', function(done) {
      var predicate = function(num) {
        return num >= 0;
      };
      var every = collections.every(list, predicate);

      every.then(function(value) {
        expect(value).to.eql(list.every(predicate));
        done();
      });
    });

    it('should return false if some do not match', function(done) {
      var predicate = function(num) {
        return num > 0;
      };
      var every = collections.every(list, predicate);

      every.then(function(value) {
        expect(value).to.eql(list.every(predicate));
        done();
      });
    });
  });

  describe('#some', function() {
    it('should have some', function() {
      expect(collections).to.have.property('some');
    });

    it('should return true if any match', function(done) {
      var predicate = function(num) {
        return num === 1;
      };
      var some = collections.some(list, predicate);

      some.then(function(value) {
        expect(value).to.eql(list.some(predicate));
        done();
      });
    });

    it('should return false if none match', function(done) {
      var predicate = function(num) {
        return num === -1;
      };
      var some = collections.some(list, predicate);

      some.then(function(value) {
        expect(value).to.eql(list.some(predicate));
        done();
      });
    });
  });

  describe('#filter', function() {
    it('should have filter', function() {
      expect(collections).to.have.property('filter');
    });

    it('should filter the elements', function(done) {
      var predicate = function(element) {
        return element > 1;
      };
      var filtered = collections.filter(list, predicate);

      filtered.then(function(value) {
        expect(value).to.eql(list.filter(predicate));
        done();
      });
    });
  });

  describe('#map', function() {
    it('should have map', function() {
      expect(collections).to.have.property('map');
    });

    it('should apply to all the elements', function(done) {
      var transform = function(element) {
        return element * 2;
      };
      var mapped = collections.map(list, transform);

      mapped.then(function(value) {
        expect(value).to.eql(list.map(transform));
        done();
      });
    });
  });

  describe('#reverse', function() {
    it('should have reverse', function() {
      expect(collections).to.have.property('reverse');
    });

    it('should apply to all the elements', function(done) {
      var reversed = collections.reverse(list);

      reversed.then(function(value) {
        expect(value).to.eql(list.reverse());
        done();
      });
    });
  });

  describe('#reduce', function() {
    it('should have reduce', function() {
      expect(collections).to.have.property('reduce');
    });

    it('should apply to all the elements', function(done) {
      var transform = function(previous, element) {
        return element * 2;
      };
      var reduced = collections.reduce(list, transform);

      reduced.then(function(value) {
        expect(value).to.eql(list.reduce(transform));
        done();
      });
    });
  });

  describe('#reduceRight', function() {
    it('should have reduceRight', function() {
      expect(collections).to.have.property('reduceRight');
    });

    it('should apply to all the elements', function(done) {
      var transform = function(previous, element) {
        return element * 2;
      };
      var reduced = collections.reduceRight(list, transform);

      reduced.then(function(value) {
        expect(value).to.eql(list.reduceRight(transform));
        done();
      });
    });
  });
});<|endoftext|>


<commit_before>(function(module) {
  'use strict';

  var user = module.parent.require('./user'),
    meta = module.parent.require('./meta'),
    db = module.parent.require('../src/database'),
    passport = module.parent.require('passport'),
    passportClientCert = require('passport-client-cert').Strategy,
    nconf = module.parent.require('nconf'),
    async = module.parent.require('async'),
    winston = module.parent.require('winston');

  var constants = Object.freeze({
    'name': 'client-cert',
    'icon': 'icon-client-cert-auth'
  });

  var CA_CNs = ['ESTEID-SK 2007', 'ESTEID-SK 2011'];
  // TODO: make a settings page in the Admin CP to configure these
  // and success/failure URLs.

  var ClientCert = {};

  ClientCert.getStrategy = function(strategies, callback) {
    passport.use(constants.name, new passportClientCert({
        passReqToCallback: true,
        renegotiation: true,
        successReturnToOrRedirect: '/',
        failureRedirect: '/client-cert-auth-error'
    }, function(req, cert, done) {
      var issuer = cert.issuer;

      if (CA_CNs.indexOf(issuer.CN) === -1) {
        winston.error('[sso-client-cert] Client Certificate Issuer CN invalid: ' + issuer.CN);
        return done(null, false);
      }

      var subject = cert.subject;

      if (!subject) {
        winston.error('[sso-client-cert] Client Certificate Subject missing!');
        return done(null, false);
      } else if (!subject.CN) {
        winston.error('[sso-client-cert] Client Certificate CN missing!');
        return done(null, false);
      }

      if (req.hasOwnProperty('user') && req.user.hasOwnProperty('uid') && req.user.uid > 0) {
        // Save cert CN to the user
        user.setUserField(req.user.uid, 'certcn', subject.CN);
        db.setObjectField('certcn:uid', subject.CN, req.user.uid);
        return done(null, req.user);
      }

      var firstName = subject.GN.charAt(0).toUpperCase() + subject.GN.slice(1).toLowerCase();
      var lastName = subject.SN.charAt(0).toUpperCase() + subject.SN.slice(1).toLowerCase();
      var userName = firstName + ' ' + lastName;
      var email = cert.subjectaltname.split('email:')[1];

      ClientCert.login(subject.CN, userName, email, function(err, user) {
        if (err) {
          return done(err);
        }

        done(null, user);
      });
    }));

    strategies.push({
      name: 'client-cert',
      url: '/auth/client-cert',
      callbackURL: '/',
      icon: constants.icon
    });

    callback(null, strategies);
  };

  ClientCert.getAssociation = function(data, callback) {
    user.getUserField(data.uid, 'certcn', function(err, CN) {
      if (err) {
        return callback(err, data);
      }

      if (CN) {
        data.associations.push({
          associated: true,
          name: constants.name,
          icon: constants.icon
        });
      } else {
        data.associations.push({
          associated: false,
          url: nconf.get('url') + '/auth/client-cert',
          name: constants.name,
          icon: constants.icon
        });
      }

      callback(null, data);
    })
  };

  ClientCert.login = function(CN, userName, email, callback) {
    ClientCert.getUidByCertCN(CN, function(err, uid) {
      if(err) {
        return callback(err);
      }

      if (uid !== null) {
        // Existing User
        callback(null, {
          uid: uid
        });
      } else {
        // New User
        var success = function(uid) {
          // Save cert CN to the user
          user.setUserField(uid, 'certcn', CN);
          db.setObjectField('certcn:uid', CN, uid);
          user.setUserField(uid, 'email:confirmed', 0); // TODO: Add option to configure in Admin CP

          callback(null, {
            uid: uid
          });
        };

        user.getUidByEmail(email, function(err, uid) {
          if(err) {
            return callback(err);
          }

          if (!uid) {
            user.create({username: userName, email: email}, function(err, uid) {
              if(err) {
                return callback(err);
              }

              success(uid);
            });
          } else {
            success(uid); // Existing account -- merge
          }
        });
      }
    });
  };

  ClientCert.getUidByCertCN = function(CN, callback) {
    db.getObjectField('certcn:uid', CN, function(err, uid) {
      if (err) {
        return callback(err);
      }
      callback(null, uid);
    });
  };

  ClientCert.deleteUserData = function(data, callback) {
    var uid = data.uid;

    async.waterfall([
      async.apply(user.getUserField, uid, 'certcn'),
      function(CNToDelete, next) {
        db.deleteObjectField('certcn:uid', CNToDelete, next);
      }
    ], function(err) {
      if (err) {
        winston.error('[sso-client-cert] Could not remove cert CN for uid ' + uid + '. Error: ' + err);
        return callback(err);
      }
      callback(null, uid);
    });
  };

  module.exports = ClientCert;
}(module));<commit_msg>Removed renegotiation: true option for now<commit_after>(function(module) {
  'use strict';

  var user = module.parent.require('./user'),
    meta = module.parent.require('./meta'),
    db = module.parent.require('../src/database'),
    passport = module.parent.require('passport'),
    passportClientCert = require('passport-client-cert').Strategy,
    nconf = module.parent.require('nconf'),
    async = module.parent.require('async'),
    winston = module.parent.require('winston');

  var constants = Object.freeze({
    'name': 'client-cert',
    'icon': 'icon-client-cert-auth'
  });

  var CA_CNs = ['ESTEID-SK 2007', 'ESTEID-SK 2011'];
  // TODO: make a settings page in the Admin CP to configure these
  // and success/failure URLs.

  var ClientCert = {};

  ClientCert.getStrategy = function(strategies, callback) {
    passport.use(constants.name, new passportClientCert({
        passReqToCallback: true,
        successReturnToOrRedirect: '/',
        failureRedirect: '/client-cert-auth-error'
    }, function(req, cert, done) {
      var issuer = cert.issuer;

      if (CA_CNs.indexOf(issuer.CN) === -1) {
        winston.error('[sso-client-cert] Client Certificate Issuer CN invalid: ' + issuer.CN);
        return done(null, false);
      }

      var subject = cert.subject;

      if (!subject) {
        winston.error('[sso-client-cert] Client Certificate Subject missing!');
        return done(null, false);
      } else if (!subject.CN) {
        winston.error('[sso-client-cert] Client Certificate CN missing!');
        return done(null, false);
      }

      if (req.hasOwnProperty('user') && req.user.hasOwnProperty('uid') && req.user.uid > 0) {
        // Save cert CN to the user
        user.setUserField(req.user.uid, 'certcn', subject.CN);
        db.setObjectField('certcn:uid', subject.CN, req.user.uid);
        return done(null, req.user);
      }

      var firstName = subject.GN.charAt(0).toUpperCase() + subject.GN.slice(1).toLowerCase();
      var lastName = subject.SN.charAt(0).toUpperCase() + subject.SN.slice(1).toLowerCase();
      var userName = firstName + ' ' + lastName;
      var email = cert.subjectaltname.split('email:')[1];

      ClientCert.login(subject.CN, userName, email, function(err, user) {
        if (err) {
          return done(err);
        }

        done(null, user);
      });
    }));

    strategies.push({
      name: 'client-cert',
      url: '/auth/client-cert',
      callbackURL: '/',
      icon: constants.icon
    });

    callback(null, strategies);
  };

  ClientCert.getAssociation = function(data, callback) {
    user.getUserField(data.uid, 'certcn', function(err, CN) {
      if (err) {
        return callback(err, data);
      }

      if (CN) {
        data.associations.push({
          associated: true,
          name: constants.name,
          icon: constants.icon
        });
      } else {
        data.associations.push({
          associated: false,
          url: nconf.get('url') + '/auth/client-cert',
          name: constants.name,
          icon: constants.icon
        });
      }

      callback(null, data);
    })
  };

  ClientCert.login = function(CN, userName, email, callback) {
    ClientCert.getUidByCertCN(CN, function(err, uid) {
      if(err) {
        return callback(err);
      }

      if (uid !== null) {
        // Existing User
        callback(null, {
          uid: uid
        });
      } else {
        // New User
        var success = function(uid) {
          // Save cert CN to the user
          user.setUserField(uid, 'certcn', CN);
          db.setObjectField('certcn:uid', CN, uid);
          user.setUserField(uid, 'email:confirmed', 0); // TODO: Add option to configure in Admin CP

          callback(null, {
            uid: uid
          });
        };

        user.getUidByEmail(email, function(err, uid) {
          if(err) {
            return callback(err);
          }

          if (!uid) {
            user.create({username: userName, email: email}, function(err, uid) {
              if(err) {
                return callback(err);
              }

              success(uid);
            });
          } else {
            success(uid); // Existing account -- merge
          }
        });
      }
    });
  };

  ClientCert.getUidByCertCN = function(CN, callback) {
    db.getObjectField('certcn:uid', CN, function(err, uid) {
      if (err) {
        return callback(err);
      }
      callback(null, uid);
    });
  };

  ClientCert.deleteUserData = function(data, callback) {
    var uid = data.uid;

    async.waterfall([
      async.apply(user.getUserField, uid, 'certcn'),
      function(CNToDelete, next) {
        db.deleteObjectField('certcn:uid', CNToDelete, next);
      }
    ], function(err) {
      if (err) {
        winston.error('[sso-client-cert] Could not remove cert CN for uid ' + uid + '. Error: ' + err);
        return callback(err);
      }
      callback(null, uid);
    });
  };

  module.exports = ClientCert;
}(module));<|endoftext|>


<commit_before>var	async = require('async'),
	XRegExp = require('xregexp').XRegExp,

	nconf = module.parent.require('nconf'),
	Topics = module.parent.require('./topics'),
	Posts = module.parent.require('./posts'),
	User = module.parent.require('./user'),
	Groups = module.parent.require('./groups'),
	Notifications = module.parent.require('./notifications'),
	Meta = module.parent.require('./meta'),
	Utils = module.parent.require('../public/src/utils'),

	SocketPlugins = module.parent.require('./socket.io/plugins'),

	regex = XRegExp('(@[\\p{L}\\d\\-_.]+)', 'g'),
	isLatinMention = /@[\w\d\-_.]+$/,
	removePunctuationSuffix = function(string) {
		return string.replace(/[!?.]*$/, '');
	},

	Mentions = {};

SocketPlugins.mentions = {};

Mentions.notify = function(postData) {
	var	_self = this,
		cleanedContent = postData.content.replace(/^>.*$/gm, ''),	// Removing blockquoted content from the checked string
		matches = cleanedContent.match(regex);

	if (matches) {

		// Eliminate duplicates
		matches = matches.filter(function(cur, idx) {
			return idx === matches.indexOf(cur);
		});

		async.filter(matches, function(match, next) {
			var	slug = Utils.slugify(match.slice(1));
			async.parallel([
				async.apply(User.exists, slug),
				async.apply(Groups.exists, match.slice(1))
			], function(err, results) {
				next(!err && results ? results[0] || results[1] : false);
			});
		}, function(matches) {
			async.parallel({
				topic: function(next) {
					Topics.getTopicFields(postData.tid, ['title'], next);
				},
				author: function(next) {
					User.getUserField(postData.uid, 'username', next);
				},
				ids: function(next) {
					async.map(matches, function(match, next) {
						var	slug = Utils.slugify(match.slice(1));

						User.getUidByUserslug(slug, function(err, uid) {
							if (err) {
								return next(err);
							}

							if (uid) {
								return next(null, uid);
							}

							next(null, match.slice(1));
						});
					}, next);
				}
			}, function(err, results) {
				var	userRecipients = results.ids.filter(function(id) {
						var	iid = parseInt(id, 10);
						return !isNaN(iid) && iid !== parseInt(postData.uid, 10);
					}),
					groupRecipients = results.ids.filter(function(id) {
						return isNaN(parseInt(id, 10));
					});

				if (!err && (userRecipients.length > 0 || groupRecipients.length > 0)) {
					Notifications.create({
						bodyShort: '[[notifications:user_mentioned_you_in, ' + results.author + ', ' + results.topic.title + ']]',
						bodyLong: postData.content,
						nid: 'tid:' + postData.tid + ':pid:' + postData.pid + ':uid:' + postData.uid,
						pid: postData.pid,
						tid: postData.tid,
						from: postData.uid,
						importance: 6
					}, function(err, notification) {
						if (err || !notification) {
							return;
						}
						if (userRecipients.length > 0) {
							Notifications.push(notification, userRecipients);
						}
						if (groupRecipients.length > 0) {
							async.each(groupRecipients, function(groupName, next) {
								Notifications.pushGroup(notification, groupName, next);
							});
						}
					});
				}
			});
		});
	}
};

Mentions.addMentions = function(data, callback) {
	if (!data || !data.postData || !data.postData.content) {
		return callback(null, data);
	}

	var relativeUrl = nconf.get('relative_url') || '';
	var matches = data.postData.content.match(regex);

	if (!matches) {
		return callback(null, data);
	}
	// Eliminate duplicates
	matches = matches.filter(function(cur, idx) {
		return idx === matches.indexOf(cur);
	});

	async.each(matches, function(match, next) {
		var slug = Utils.slugify(match.slice(1));

		match = removePunctuationSuffix(match);

		async.parallel({
			groupName: async.apply(Groups.exists, slug),
			uid: async.apply(User.getUidByUserslug, slug)
		}, function(err, results) {
			if (err) {
				return next(err);
			}

			if (results.uid) {
				if (isLatinMention.test(match)) {
					data.postData.content = data.postData.content.replace(new RegExp(match + '\\b', 'g'), '' + match + '');
				} else {
					data.postData.content = data.postData.content.replace(new RegExp(match, 'g'), '' + match + '');
				}
			} else if (results.groupName) {
				data.postData.content = data.postData.content.replace(new RegExp(match + '\\b', 'g'), '' + match + '');
			}

			next();
		});
	}, function(err) {
		callback(err, data);
	});
};

/*
	WebSocket methods
*/

SocketPlugins.mentions.listGroups = function(socket, data, callback) {
	Groups.list({
		removeEphemeralGroups: true,
		truncateUserList: true
	}, function(err, groups) {
		if (err || !Array.isArray(groups)) {
			return callback(null, []);
		}
		callback(null, groups.map(function(groupObj) {
			return groupObj.name;
		}));
	});
};

module.exports = Mentions;
<commit_msg>properly cleaning input so that mentions aren't captured in blockquotes for notification, and blockquotes and preformatted text, for highlighting<commit_after>var	async = require('async'),
	XRegExp = require('xregexp').XRegExp,

	nconf = module.parent.require('nconf'),
	Topics = module.parent.require('./topics'),
	Posts = module.parent.require('./posts'),
	User = module.parent.require('./user'),
	Groups = module.parent.require('./groups'),
	Notifications = module.parent.require('./notifications'),
	Meta = module.parent.require('./meta'),
	Utils = module.parent.require('../public/src/utils'),

	SocketPlugins = module.parent.require('./socket.io/plugins'),

	regex = XRegExp('(@[\\p{L}\\d\\-_.]+)', 'g'),
	isLatinMention = /@[\w\d\-_.]+$/,
	removePunctuationSuffix = function(string) {
		return string.replace(/[!?.]*$/, '');
	},

	Mentions = {};

SocketPlugins.mentions = {};

Mentions.notify = function(postData) {
	var	_self = this,
		cleanedContent = Mentions.clean(postData.content, true, true, true);
		matches = cleanedContent.match(regex);

	if (matches) {

		// Eliminate duplicates
		matches = matches.filter(function(cur, idx) {
			return idx === matches.indexOf(cur);
		});

		async.filter(matches, function(match, next) {
			var	slug = Utils.slugify(match.slice(1));
			async.parallel([
				async.apply(User.exists, slug),
				async.apply(Groups.exists, match.slice(1))
			], function(err, results) {
				next(!err && results ? results[0] || results[1] : false);
			});
		}, function(matches) {
			async.parallel({
				topic: function(next) {
					Topics.getTopicFields(postData.tid, ['title'], next);
				},
				author: function(next) {
					User.getUserField(postData.uid, 'username', next);
				},
				ids: function(next) {
					async.map(matches, function(match, next) {
						var	slug = Utils.slugify(match.slice(1));

						User.getUidByUserslug(slug, function(err, uid) {
							if (err) {
								return next(err);
							}

							if (uid) {
								return next(null, uid);
							}

							next(null, match.slice(1));
						});
					}, next);
				}
			}, function(err, results) {
				var	userRecipients = results.ids.filter(function(id) {
						var	iid = parseInt(id, 10);
						return !isNaN(iid) && iid !== parseInt(postData.uid, 10);
					}),
					groupRecipients = results.ids.filter(function(id) {
						return isNaN(parseInt(id, 10));
					});

				if (!err && (userRecipients.length > 0 || groupRecipients.length > 0)) {
					Notifications.create({
						bodyShort: '[[notifications:user_mentioned_you_in, ' + results.author + ', ' + results.topic.title + ']]',
						bodyLong: postData.content,
						nid: 'tid:' + postData.tid + ':pid:' + postData.pid + ':uid:' + postData.uid,
						pid: postData.pid,
						tid: postData.tid,
						from: postData.uid,
						importance: 6
					}, function(err, notification) {
						if (err || !notification) {
							return;
						}
						if (userRecipients.length > 0) {
							Notifications.push(notification, userRecipients);
						}
						if (groupRecipients.length > 0) {
							async.each(groupRecipients, function(groupName, next) {
								Notifications.pushGroup(notification, groupName, next);
							});
						}
					});
				}
			});
		});
	}
};

Mentions.addMentions = function(data, callback) {
	if (!data || !data.postData || !data.postData.content) {
		return callback(null, data);
	}

	var relativeUrl = nconf.get('relative_url') || '',
		cleanedContent = Mentions.clean(data.postData.content, false, false, true),
		matches = cleanedContent.match(regex);

	if (!matches) {
		return callback(null, data);
	}
	// Eliminate duplicates
	matches = matches.filter(function(cur, idx) {
		return idx === matches.indexOf(cur);
	});

	async.each(matches, function(match, next) {
		var slug = Utils.slugify(match.slice(1));

		match = removePunctuationSuffix(match);

		async.parallel({
			groupName: async.apply(Groups.exists, slug),
			uid: async.apply(User.getUidByUserslug, slug)
		}, function(err, results) {
			if (err) {
				return next(err);
			}

			if (results.uid) {
				if (isLatinMention.test(match)) {
					data.postData.content = data.postData.content.replace(new RegExp(match + '\\b', 'g'), '' + match + '');
				} else {
					data.postData.content = data.postData.content.replace(new RegExp(match, 'g'), '' + match + '');
				}
			} else if (results.groupName) {
				data.postData.content = data.postData.content.replace(new RegExp(match + '\\b', 'g'), '' + match + '');
			}

			next();
		});
	}, function(err) {
		callback(err, data);
	});
};

Mentions.clean = function(input, isMarkdown, stripBlockquote, stripPreformatted) {
	var bqMatch = isMarkdown ? /^>.*$/gm : /^
.*<\/blockquote>/gm,
		pfMatch = isMarkdown ? /`[^`\n]+`/gm : /^
.*<\/code><\/p>/gm;

	if (stripBlockquote) {
		input = input.replace(bqMatch, '');
	}
	if (stripPreformatted) {
		input = input.replace(pfMatch, '');
	}

	return input;
};

/*
	WebSocket methods
*/

SocketPlugins.mentions.listGroups = function(socket, data, callback) {
	Groups.list({
		removeEphemeralGroups: true,
		truncateUserList: true
	}, function(err, groups) {
		if (err || !Array.isArray(groups)) {
			return callback(null, []);
		}
		callback(null, groups.map(function(groupObj) {
			return groupObj.name;
		}));
	});
};

module.exports = Mentions;
<|endoftext|>


<commit_before>var assert = require('assert'),
    ConnectionHelper = require('../connection_helper.js'),
    SentinelHelper = require('simple_sentinel'),
    client;

describe('given a ConnectionHelper', function() {
  var configuration = {
    connection_settings: {
      redis: {
        host: 'localhost',
        port: 6379
      },
      sentinel: {
        // sentinel master name is required
        id: 'mymaster',
        sentinels: [
        {
          host: 'localhost',
          port: 26379
        },
        {
          host: 'localhost',
          port: 26380
        },
        {
          host: 'localhost',
          port: 26381
        }]
      }
    }
  };

  describe('with redis configuration', function() {
    it('should connect', function(done) {
      var config = JSON.parse(JSON.stringify(configuration));
      config.use_connection = 'redis';

      var connection = ConnectionHelper.connection(config);
      connection.establish(function() {
        assert.deepEqual(connection.config,  { host: 'localhost', port: 6379 });
        ConnectionHelper.destroyConnection(config, done);
      });
    });
    it('should reuse existing connection', function(done) {
      var config = JSON.parse(JSON.stringify(configuration));
      config.use_connection = 'redis';

      var connection = ConnectionHelper.connection(config);
      connection.establish(function() {
        assert.deepEqual(connection.config,  { host: 'localhost', port: 6379 });
        assert.deepEqual(connection, ConnectionHelper.connection(config));
        ConnectionHelper.destroyConnection(config, done);
      });
    });
  });

  describe('with sentinel configuration', function() {
      var sentinel_config = {
        redis : {
          ports: [ 16379, 16380, 16381 ]
        },
        sentinel: {
          ports: [ 26379, 26380, 26381 ]
        }
      };
    before(function() {
      process.env.noverbose=true;
      SentinelHelper.start(sentinel_config);
    });
    after(function() {
      process.env.noverbose=true;
      SentinelHelper.stop(sentinel_config);
    });

    it('should connect', function(done) {
      var config = JSON.parse(JSON.stringify(configuration));
      config.use_connection = 'sentinel';

      var connection = ConnectionHelper.connection(config);
      connection.establish(function() {
        assert.deepEqual(connection.config,  {
          id:"mymaster",
          sentinels:[{host:"localhost",port:26379},
                     {host:"localhost",port:26380},
                     {host:"localhost",port:26381}]
        });
        ConnectionHelper.destroyConnection(config, done);
      });
    });
    it('should reuse existing connection', function(done) {
      var config = JSON.parse(JSON.stringify(configuration));
      config.use_connection = 'sentinel';

      var connection = ConnectionHelper.connection(config);
      connection.establish(function() {
        assert.deepEqual(connection.config,  {
          id:"mymaster",
          sentinels:[{host:"localhost",port:26379},
                     {host:"localhost",port:26380},
                     {host:"localhost",port:26381}]
        });
        assert.deepEqual(connection, ConnectionHelper.connection(config));
        ConnectionHelper.destroyConnection(config, done);
      });
    });
  });

  describe('given a connection', function() {
    var connection;
    var config = JSON.parse(JSON.stringify(configuration));
    config.use_connection = 'redis';

    before(function(done) {
      connection = ConnectionHelper.connection(config);
      connection.establish(done);
    });
    after(function(done) {
      ConnectionHelper.destroyConnection(config,done);
    });
    it('should not create different clients for multiple establish calls', function(done) {
      var client1, client2;
      connection.establish(function() {
        client1 = connection.client;
      });
      connection.establish(function() {
        client2 = connection.client;
        assert.equal(client1, client2);
        done();
      });
    });
  });

});
<commit_msg>setup our own redis for connection testing<commit_after>var assert = require('assert'),
    ConnectionHelper = require('../connection_helper.js'),
    SentinelHelper = require('simple_sentinel'),
    client;

describe('given a ConnectionHelper', function() {
  var configuration = {
    connection_settings: {
      redis: {
        host: 'localhost',
        port: 16379
      },
      sentinel: {
        // sentinel master name is required
        id: 'mymaster',
        sentinels: [
        {
          host: 'localhost',
          port: 26379
        },
        {
          host: 'localhost',
          port: 26380
        },
        {
          host: 'localhost',
          port: 26381
        }]
      }
    }
  };

  describe('with redis configuration', function() {
      var helper_config = {
        redis : {
          ports: [ 16379 ]
        },
      };
    before(function() {
      process.env.noverbose=true;
      SentinelHelper.start(helper_config);
    });
    after(function() {
      process.env.noverbose=true;
      SentinelHelper.stop(helper_config);
    });
    it('should connect', function(done) {
      var config = JSON.parse(JSON.stringify(configuration));
      config.use_connection = 'redis';

      var connection = ConnectionHelper.connection(config);
      connection.establish(function() {
        assert.deepEqual(connection.config,  { host: 'localhost', port: 16379 });
        ConnectionHelper.destroyConnection(config, done);
      });
    });
    it('should reuse existing connection', function(done) {
      var config = JSON.parse(JSON.stringify(configuration));
      config.use_connection = 'redis';

      var connection = ConnectionHelper.connection(config);
      connection.establish(function() {
        assert.deepEqual(connection.config,  { host: 'localhost', port: 16379 });
        assert.deepEqual(connection, ConnectionHelper.connection(config));
        ConnectionHelper.destroyConnection(config, done);
      });
    });
  });

  describe('with sentinel configuration', function() {
      var helper_config = {
        redis : {
          ports: [ 16379, 16380, 16381 ]
        },
        sentinel: {
          ports: [ 26379, 26380, 26381 ]
        }
      };
    before(function() {
      process.env.noverbose=true;
      SentinelHelper.start(helper_config);
    });
    after(function() {
      process.env.noverbose=true;
      SentinelHelper.stop(helper_config);
    });

    it('should connect', function(done) {
      var config = JSON.parse(JSON.stringify(configuration));
      config.use_connection = 'sentinel';

      var connection = ConnectionHelper.connection(config);
      connection.establish(function() {
        assert.deepEqual(connection.config,  {
          id:"mymaster",
          sentinels:[{host:"localhost",port:26379},
                     {host:"localhost",port:26380},
                     {host:"localhost",port:26381}]
        });
        ConnectionHelper.destroyConnection(config, done);
      });
    });
    it('should reuse existing connection', function(done) {
      var config = JSON.parse(JSON.stringify(configuration));
      config.use_connection = 'sentinel';

      var connection = ConnectionHelper.connection(config);
      connection.establish(function() {
        assert.deepEqual(connection.config,  {
          id:"mymaster",
          sentinels:[{host:"localhost",port:26379},
                     {host:"localhost",port:26380},
                     {host:"localhost",port:26381}]
        });
        assert.deepEqual(connection, ConnectionHelper.connection(config));
        ConnectionHelper.destroyConnection(config, done);
      });
    });
  });

  describe('given a connection', function() {
    var connection;
    var config = JSON.parse(JSON.stringify(configuration));
    config.use_connection = 'redis';

    var helper_config = {
      redis : {
        ports: [ 16379 ]
      },
    };
    before(function(done) {
      process.env.noverbose=true;
      SentinelHelper.start(helper_config);
      connection = ConnectionHelper.connection(config);
      connection.establish(done);
    });
    after(function(done) {
      ConnectionHelper.destroyConnection(config,done);
      process.env.noverbose=true;
      SentinelHelper.stop(helper_config);
    });
    it('should not create different clients for multiple establish calls', function(done) {
      var client1, client2;
      connection.establish(function() {
        client1 = connection.client;
      });
      connection.establish(function() {
        client2 = connection.client;
        assert.equal(client1, client2);
        done();
      });
    });
  });

});
<|endoftext|>


<commit_before>var fs = require('fsplus');
var path = require('path');
var geci = require('geci');
var mkdirp = require('mkdirp');
var Player = require('player');
var color = require('colorful');
var consoler = require('consoler');
var termList = require('term-list-enhanced');
var sdk = require('./sdk');
var utils = require('./utils');
var pkg = require('../package');
var errors = require('./errors');
var template = require('./template');

// the keypress shortcut list
var shorthands = {
  'return': 'play',
  'backspace': 'stop',
  'g': 'go',
  'l': 'loving',
  'n': 'next',
  'q': 'quit',
  's': 'share',
  'r': 'showLrc'
};

module.exports = Fm;

function Fm() {
  this.rc = {};
  this.syshome = utils.home();
  this.rc.profile = path.join(this.syshome, '.douban.fm.profile.json');
  this.rc.history = path.join(this.syshome, '.douban.fm.history.json');
  this.home = utils.readJSON(this.rc.profile).home || path.join(this.syshome, 'douban.fm');
  this.http_proxy = utils.readJSON(this.rc.profile).http_proxy || null;
  this.love = path.join(this.home, 'love');
  this.isShowLrc = false;
  template.updateTab('Douban FM');
  // ensure dir exists
  try {
    mkdirp.sync(this.love);
  } catch (err) {
    consoler.error(errors.setup_fail);
    throw err;
  }
};

function isChannel(alias, id) {
  if (alias === 'local' && id == -99) return true;
  if (alias === 'private' && (id == 0 || id == -3)) return true;
  return false;
}

/**
*
* Fetch songs and add them to playlist
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.fetch = function(channel, account, callback) {
  var self = this;
  var query = {};
  query.kbps = 192;
  query.history = self.rc.history;
  query.channel = channel.channel_id;
  query.local = isChannel('local', channel.channel_id) ? self.home : false;
  if (account) {
    query.token = account.token;
    query.user_id = account.user_id;
    query.expire = account.expire;
  }

  return sdk.songs(query, utils.isFunction(callback) ? callback : cb);

  function cb(err, songs, result) {
    if (err) return;
    if (!songs || songs.length === 0) return;
    if (!self.player) return;
    songs.forEach(function(song) {
      self.player.add(song);
    });
  }
}

/**
*
* Playing songs when everything is ready
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.play = function(channel, account) {
  var self = this;
  var menu = self.menu;
  var isVaildAccount = account && account.token;
  var privateMhz = isChannel('private', channel.channel_id) && !isVaildAccount;

  // Check if this kind of mHz is private
  if (privateMhz) return menu.update('header', errors.account_missing);

  // clear last label
  if (self.status === 'fetching' || self.status === 'downloading') return;
  if (self.status === 'playing' || self.status === 'error') {
    if (typeof(self.channel) != undefined) menu.clear(self.channel);
    if (self.player) {
      self.player.stop();
      delete self.player;
    }
  }

  // clear label status
  menu.clear('header');
  self.channel = channel.index;
  self.status = 'fetching';
  menu.update(channel.index, template.listing());
  try {
    fs.updateJSON(self.rc.profile, { lastChannel: channel });
  } catch (err) {};

  // start fetching songs
  self.fetch(channel, account, function(err, songs, result) {
    if (err) {
      self.status = 'error';
      return menu.update(channel.index, color.red(err.toString()));
    }
    // mark PRO account on logo
    if (result && !result.warning) menu.update('header', color.inverse(' PRO '));
    self.status = 'ready';
    self.player = new Player(songs, {
      src: 'url',
      cache: true,
      downloads: self.home,
      http_proxy: self.http_proxy
    });
    self.player.play();
    self.player.on('downloading', function(url) {
      self.status = 'downloading';
      menu.update(channel.index, template.loading());
    });
    // update template
    self.player.on('playing', function(song) {
      var isValidSong = song.title && song.sid;
      self.status = 'playing';
      // update playing label
      menu.update('header', color.green('>'));
      // update song infomation
      menu.update(channel.index, template.song(song));
      // logging songs history
      if (isValidSong) {
        var updates = {};
        updates[song.sid] = song;
        try {
          fs.updateJSON(this.rc.history, updates);
        } catch (err) {
          // error must be logged in a private place.
        }
      }
      // print LRC if needed.
      if (self.isShowLrc) {
        if (self.lrc) self.lrc.stop();
        geci.fetch(song, function(err, lrc) {
          if (err) return menu.update('header', color.grey(errors.lrc_notfound + err.toString()));
          if (!lrc) return menu.update('header', color.grey(errors.lrc_notfound));
          self.lrc = geci.print(lrc, function(line, extra) {
            menu.update(channel.index, template.song(song, line));
          });
        });
      }
      // 没有对尝试获取列表失败进行处理，如果失败2次，则不会再播放任何歌曲
      if (song._id < self.player.list.length - 1) return false;
      return self.fetch(channel, account);
    });
  });
}

/**
*
* Add current song to lovelist when pressing `L`
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.loving = function(channel, account) {
  if (!this.player) return false;
  if (!this.player.playing) return false;
  if (!this.player.playing.sid) return this.menu.update('header', errors.love_fail);
  if (!account) return this.menu.update('header', errors.account_missing);
  var self = this;
  var menu = self.menu;
  var song = self.player.playing;
  var query = {
    sid: song.sid,
    channel: self.channel,
    user_id: account.user_id,
    expire: account.expire,
    token: account.token
  };
  if (song.like) query.type = 'u';
  menu.update('header', '正在加载...');
  sdk.love(query, function(err, result) {
    menu.clear('header');
    if (err) menu.update('header', errors.normal);
    if (!err) self.player.playing.like = !song.like;
    return menu.update(
      self.channel,
      template.song(self.player.playing, null, true) // keep silence, do not notify
    );
  });
}

/**
*
* Play the next song in the playlist
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.next = function(channel, account) {
  if (!this.player) return false;
  var menu = this.menu;
  this.player.next(function(err, song) {
    if (err) menu.update('header', errors.last_song);
    return false;
  });
}

/**
*
* Stop playing
* and show the stopped status on logo.
*
**/
Fm.prototype.stop = function(channel, account) {
  if (!this.player) return false;
  if (this.status === 'stopped') return this.play(channel, account);
  var menu = this.menu;
  menu.clear('header');
  menu.update('header', template.pause());
  this.status = 'stopped';
  return this.player.stop();
}

/**
*
* Quit the Fm
* and kill the process when pressing `Q`
*
**/
Fm.prototype.quit = function() {
  this.menu.stop();
  return process.exit();
}

/**
*
* Goto the music album page when pressing `G`
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.go = function(channel, account) {
  if (!this.player) return false;
  if (!this.player.playing) return false;
  if (channel.channel_id == -99) return false;
  return utils.go(utils.album(this.player.playing.album));
}

/**
*
* Show lrc when when pressing `R`.
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.showLrc = function(channel, account) {
  if (channel.channel_id == -99) return false;
  this.isShowLrc = !!!this.isShowLrc;
  this.menu.clear('header');
  this.menu.update('header', this.isShowLrc ? '歌词开启' : '歌词关闭');
  return false;
}

/**
*
* Share the current playing songs to Weibo when pressing `S`.
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.share = function(channel, account) {
  if (!this.player || !this.player.playing) return false;
  return utils.go(template.share(this.player.playing));
}

/**
*
* Create command line interface menu
* using term-list-enhanced module
* @callback[Function]: the callback function when set down.
*
**/
Fm.prototype.createMenu = function(callback) {
  var self = this;
  // fetch channels
  sdk.fm.channels(function(err, list) {
    if (err) consoler.error(errors.turn_to_local_mode);
    // fetch configs, show user's infomations
    fs.readJSON(self.rc.profile, function(e, user) {
      var vaildAccount = user && user.account && user.account.user_name;
      var account = vaildAccount ? user.account : null;
      // init menu
      self.menu = new termList();
      self.menu.header(template.logo(account));
      var nav = [sdk.mhz.localMhz];
      self.menu.adds(nav.concat(!err ? [sdk.mhz.privateMhz].concat(list) : []));
      // bind keypress events
      self.menu.on('keypress', function(key, index) {
        if (!shorthands[key.name]) return false;
        return self[shorthands[key.name]](self.menu.items[index], account);
      });
      self.menu.on('empty', function() {
        self.menu.stop();
      });
      // check last played channel
      if (user && user.lastChannel) {
        self.play(user.lastChannel, account);
        self.menu.start(user.lastChannel.index);
        return false;
      }
      // start menu at line 2 (below the logo text)
      self.menu.start(1);
    });
  });
  // callback if necessary.
  return callback && callback();
}

/**
*
* Init douban.fm command line interface.
* @callback [Function]: the callback function when all set done
*
**/
Fm.prototype.init = function(callback) {
  var self = this;
  fs.exists(self.home, function(exist) {
    if (exist) return self.createMenu(callback);
    mkdirp(self.love, function(err) {
      if (err) return consoler.error(errors.mkdir_fail);
      return self.createMenu(callback);
    });
  });
}
<commit_msg>fix无法读取 .douban.fm.history.json 的问题，但该文件现在依旧需手动创建<commit_after>var fs = require('fsplus');
var path = require('path');
var geci = require('geci');
var mkdirp = require('mkdirp');
var Player = require('player');
var color = require('colorful');
var consoler = require('consoler');
var termList = require('term-list-enhanced');
var sdk = require('./sdk');
var utils = require('./utils');
var pkg = require('../package');
var errors = require('./errors');
var template = require('./template');

// the keypress shortcut list
var shorthands = {
  'return': 'play',
  'backspace': 'stop',
  'g': 'go',
  'l': 'loving',
  'n': 'next',
  'q': 'quit',
  's': 'share',
  'r': 'showLrc'
};

module.exports = Fm;

function Fm() {
  this.rc = {};
  this.syshome = utils.home();
  this.rc.profile = path.join(this.syshome, '.douban.fm.profile.json');
  this.rc.history = path.join(this.syshome, '.douban.fm.history.json');
  this.home = utils.readJSON(this.rc.profile).home || path.join(this.syshome, 'douban.fm');
  this.http_proxy = utils.readJSON(this.rc.profile).http_proxy || null;
  this.love = path.join(this.home, 'love');
  this.isShowLrc = false;
  template.updateTab('Douban FM');
  // ensure dir exists
  try {
    mkdirp.sync(this.love);
  } catch (err) {
    consoler.error(errors.setup_fail);
    throw err;
  }
};

function isChannel(alias, id) {
  if (alias === 'local' && id == -99) return true;
  if (alias === 'private' && (id == 0 || id == -3)) return true;
  return false;
}

/**
*
* Fetch songs and add them to playlist
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.fetch = function(channel, account, callback) {
  var self = this;
  var query = {};
  query.kbps = 192;
  query.history = self.rc.history;
  query.channel = channel.channel_id;
  query.local = isChannel('local', channel.channel_id) ? self.home : false;
  if (account) {
    query.token = account.token;
    query.user_id = account.user_id;
    query.expire = account.expire;
  }

  return sdk.songs(query, utils.isFunction(callback) ? callback : cb);

  function cb(err, songs, result) {
    if (err) return;
    if (!songs || songs.length === 0) return;
    if (!self.player) return;
    songs.forEach(function(song) {
      self.player.add(song);
    });
  }
}

/**
*
* Playing songs when everything is ready
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.play = function(channel, account) {
  var self = this;
  var menu = self.menu;
  var isVaildAccount = account && account.token;
  var privateMhz = isChannel('private', channel.channel_id) && !isVaildAccount;

  // Check if this kind of mHz is private
  if (privateMhz) return menu.update('header', errors.account_missing);

  // clear last label
  if (self.status === 'fetching' || self.status === 'downloading') return;
  if (self.status === 'playing' || self.status === 'error') {
    if (typeof(self.channel) != undefined) menu.clear(self.channel);
    if (self.player) {
      self.player.stop();
      delete self.player;
    }
  }

  // clear label status
  menu.clear('header');
  self.channel = channel.index;
  self.status = 'fetching';
  menu.update(channel.index, template.listing());
  try {
    fs.updateJSON(self.rc.profile, { lastChannel: channel });
  } catch (err) {};

  // start fetching songs
  self.fetch(channel, account, function(err, songs, result) {
    if (err) {
      self.status = 'error';
      return menu.update(channel.index, color.red(err.toString()));
    }
    // mark PRO account on logo
    if (result && !result.warning) menu.update('header', color.inverse(' PRO '));
    self.status = 'ready';
    self.player = new Player(songs, {
      src: 'url',
      cache: true,
      downloads: self.home,
      http_proxy: self.http_proxy
    });
    self.player.play();
    self.player.on('downloading', function(url) {
      self.status = 'downloading';
      menu.update(channel.index, template.loading());
    });
    // update template
    self.player.on('playing', function(song) {
      var isValidSong = song.title && song.sid;
      self.status = 'playing';
      // update playing label
      menu.update('header', color.green('>'));
      // update song infomation
      menu.update(channel.index, template.song(song));
      // logging songs history
      if (isValidSong) {
        var updates = {};
        updates[song.sid] = song;
        try {
          fs.updateJSON(self.rc.history, updates);
        } catch (err) {
          // error must be logged in a private place.
        }
      }
      // print LRC if needed.
      if (self.isShowLrc) {
        if (self.lrc) self.lrc.stop();
        geci.fetch(song, function(err, lrc) {
          if (err) return menu.update('header', color.grey(errors.lrc_notfound + err.toString()));
          if (!lrc) return menu.update('header', color.grey(errors.lrc_notfound));
          self.lrc = geci.print(lrc, function(line, extra) {
            menu.update(channel.index, template.song(song, line));
          });
        });
      }
      // 没有对尝试获取列表失败进行处理，如果失败2次，则不会再播放任何歌曲
      if (song._id < self.player.list.length - 1) return false;
      return self.fetch(channel, account);
    });
  });
}

/**
*
* Add current song to lovelist when pressing `L`
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.loving = function(channel, account) {
  if (!this.player) return false;
  if (!this.player.playing) return false;
  if (!this.player.playing.sid) return this.menu.update('header', errors.love_fail);
  if (!account) return this.menu.update('header', errors.account_missing);
  var self = this;
  var menu = self.menu;
  var song = self.player.playing;
  var query = {
    sid: song.sid,
    channel: self.channel,
    user_id: account.user_id,
    expire: account.expire,
    token: account.token
  };
  if (song.like) query.type = 'u';
  menu.update('header', '正在加载...');
  sdk.love(query, function(err, result) {
    menu.clear('header');
    if (err) menu.update('header', errors.normal);
    if (!err) self.player.playing.like = !song.like;
    return menu.update(
      self.channel,
      template.song(self.player.playing, null, true) // keep silence, do not notify
    );
  });
}

/**
*
* Play the next song in the playlist
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.next = function(channel, account) {
  if (!this.player) return false;
  var menu = this.menu;
  this.player.next(function(err, song) {
    if (err) menu.update('header', errors.last_song);
    return false;
  });
}

/**
*
* Stop playing
* and show the stopped status on logo.
*
**/
Fm.prototype.stop = function(channel, account) {
  if (!this.player) return false;
  if (this.status === 'stopped') return this.play(channel, account);
  var menu = this.menu;
  menu.clear('header');
  menu.update('header', template.pause());
  this.status = 'stopped';
  return this.player.stop();
}

/**
*
* Quit the Fm
* and kill the process when pressing `Q`
*
**/
Fm.prototype.quit = function() {
  this.menu.stop();
  return process.exit();
}

/**
*
* Goto the music album page when pressing `G`
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.go = function(channel, account) {
  if (!this.player) return false;
  if (!this.player.playing) return false;
  if (channel.channel_id == -99) return false;
  return utils.go(utils.album(this.player.playing.album));
}

/**
*
* Show lrc when when pressing `R`.
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.showLrc = function(channel, account) {
  if (channel.channel_id == -99) return false;
  this.isShowLrc = !!!this.isShowLrc;
  this.menu.clear('header');
  this.menu.update('header', this.isShowLrc ? '歌词开启' : '歌词关闭');
  return false;
}

/**
*
* Share the current playing songs to Weibo when pressing `S`.
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.share = function(channel, account) {
  if (!this.player || !this.player.playing) return false;
  return utils.go(template.share(this.player.playing));
}

/**
*
* Create command line interface menu
* using term-list-enhanced module
* @callback[Function]: the callback function when set down.
*
**/
Fm.prototype.createMenu = function(callback) {
  var self = this;
  // fetch channels
  sdk.fm.channels(function(err, list) {
    if (err) consoler.error(errors.turn_to_local_mode);
    // fetch configs, show user's infomations
    fs.readJSON(self.rc.profile, function(e, user) {
      var vaildAccount = user && user.account && user.account.user_name;
      var account = vaildAccount ? user.account : null;
      // init menu
      self.menu = new termList();
      self.menu.header(template.logo(account));
      var nav = [sdk.mhz.localMhz];
      self.menu.adds(nav.concat(!err ? [sdk.mhz.privateMhz].concat(list) : []));
      // bind keypress events
      self.menu.on('keypress', function(key, index) {
        if (!shorthands[key.name]) return false;
        return self[shorthands[key.name]](self.menu.items[index], account);
      });
      self.menu.on('empty', function() {
        self.menu.stop();
      });
      // check last played channel
      if (user && user.lastChannel) {
        self.play(user.lastChannel, account);
        self.menu.start(user.lastChannel.index);
        return false;
      }
      // start menu at line 2 (below the logo text)
      self.menu.start(1);
    });
  });
  // callback if necessary.
  return callback && callback();
}

/**
*
* Init douban.fm command line interface.
* @callback [Function]: the callback function when all set done
*
**/
Fm.prototype.init = function(callback) {
  var self = this;
  fs.exists(self.home, function(exist) {
    if (exist) return self.createMenu(callback);
    mkdirp(self.love, function(err) {
      if (err) return consoler.error(errors.mkdir_fail);
      return self.createMenu(callback);
    });
  });
}
<|endoftext|>


<commit_before>/* Test flow
1. Use a local html page to see if parser could extract correct message from the page.
 */
const cur = require('../../app/thulib/curriculum')
const readFile = require('fs-readfile-promise')
const cheerio = require('cheerio')

describe('1. test parser', () => {
  it('1.1 curriculum info should be properly extracted form the right test page', async () =>{
    const data = await readFile(`${__dirname}\\test-right.html`, 'utf8')
    const $ = cheerio.load(data, { decodeEntities: false })
    const classes = await cur.parseFirstLevelCurriculum($)
    classes.should.be.Array().and.should.not.be.empty()

    const lesson = classes[0]
    const properties = ['courseid', 'coursename', 'time', 'teacher', 'classroom', 'week']
    lesson.should.have.properties(properties)

    const time = lesson.time
    time.should.be.Array().and.have.length(2)
    time[0].should.be.Number().and.within(1, 7)
    time[1].should.be.Number().and.within(1, 6)

    const week = lesson.week
    week.should.be.Array().and.have.length(16)
    week.forEach((ele) => {
      ele.should.be.Number().and.within(0, 1)
    })
  })

  //FIXME: How to test an exception threwed by async function properly
  /*
  it('1.2 curriculum info should be empty from the wrong test page', async (done) =>{
    const data = await readFile(`${__dirname}\\test-wrong.html`, 'utf8')
    const $ = cheerio.load(data, { decodeEntities: false })

    const myFunc = () => { cur.parseFirstLevelCurriculum($) }
    assert.throws(myFunc)
  })
  */
})
<commit_msg>modify the fixme comment<commit_after>/* Test flow
1. Use a local html page to see if parser could extract correct message from the page.
 */
const cur = require('../../app/thulib/curriculum')
const readFile = require('fs-readfile-promise')
const cheerio = require('cheerio')

describe('1. test parser', () => {
  it('1.1 curriculum info should be properly extracted form the right test page', async () =>{
    const data = await readFile(`${__dirname}\\test-right.html`, 'utf8')
    const $ = cheerio.load(data, { decodeEntities: false })
    const classes = await cur.parseFirstLevelCurriculum($)
    classes.should.be.Array().and.should.not.be.empty()

    const lesson = classes[0]
    const properties = ['courseid', 'coursename', 'time', 'teacher', 'classroom', 'week']
    lesson.should.have.properties(properties)

    const time = lesson.time
    time.should.be.Array().and.have.length(2)
    time[0].should.be.Number().and.within(1, 7)
    time[1].should.be.Number().and.within(1, 6)

    const week = lesson.week
    week.should.be.Array().and.have.length(16)
    week.forEach((ele) => {
      ele.should.be.Number().and.within(0, 1)
    })
  })

  //FIXME: How to test an exception thrown by async function properly
  /*
  it('1.2 curriculum info should be empty from the wrong test page', async (done) =>{
    const data = await readFile(`${__dirname}\\test-wrong.html`, 'utf8')
    const $ = cheerio.load(data, { decodeEntities: false })

    const myFunc = () => { cur.parseFirstLevelCurriculum($) }
    assert.throws(myFunc)
  })
  */
})
<|endoftext|>


<commit_before>import './support';
import fs from 'fs';
import path from 'path';
import rimraf from 'rimraf';
import {getInstalledKeyboardLanguages} from 'keyboard-layout';

import DictionarySync from '../src/dictionary-sync';

let testCount = 0;

describe('The Dictionary Sync class', function() {
  beforeEach(function() {
    this.tempCacheDir = path.join(__dirname, `__dict_sync_${testCount++}`);
    this.fixture = new DictionarySync(this.tempCacheDir);
  });

  afterEach(function() {
    rimraf.sync(this.tempCacheDir);
  });

  describe('loadDictionaryForLanguage method', function() {
    this.timeout(60*1000);

    it('should download the German dictionary', async function() {
      let buf = await this.fixture.loadDictionaryForLanguage('de-DE');

      expect(buf.constructor.name).to.equal('Buffer');
      expect(buf.length > 1000).to.be.ok;
    });
  });

  describe('preloadDictionaries', function() {
    this.timeout(60*1000);

    it('should preload some dictionaries', async function() {
      let langFiles = await this.fixture.preloadDictionaries();

      expect(langFiles.length).to.equal(getInstalledKeyboardLanguages().length);
      for (let lang of langFiles) {
        expect(fs.existsSync(lang)).to.be.ok;
      }
    });
  });
});
<commit_msg>Ugh whatever Linux, you're not my real dad<commit_after>import './support';
import fs from 'fs';
import path from 'path';
import rimraf from 'rimraf';
import {getInstalledKeyboardLanguages} from 'keyboard-layout';

import DictionarySync from '../src/dictionary-sync';

let testCount = 0;

describe('The Dictionary Sync class', function() {
  beforeEach(function() {
    this.tempCacheDir = path.join(__dirname, `__dict_sync_${testCount++}`);
    this.fixture = new DictionarySync(this.tempCacheDir);
  });

  afterEach(function() {
    rimraf.sync(this.tempCacheDir);
  });

  describe('loadDictionaryForLanguage method', function() {
    this.timeout(60*1000);

    it('should download the German dictionary', async function() {
      let buf = await this.fixture.loadDictionaryForLanguage('de-DE');

      expect(buf.constructor.name).to.equal('Buffer');
      expect(buf.length > 1000).to.be.ok;
    });
  });

  describe('preloadDictionaries', function() {
    this.timeout(60*1000);

    it('should preload some dictionaries', async function() {
      if (process.platform === 'linux') return;

      let installedLangs = getInstalledKeyboardLanguages();
      if (!installedLangs || installedLangs.length < 1) return;

      let langFiles = await this.fixture.preloadDictionaries();

      expect(langFiles.length).to.equal(installedLangs.length);
      for (let lang of langFiles) {
        expect(fs.existsSync(lang)).to.be.ok;
      }
    });
  });
});
<|endoftext|>