qiu8310/ylog

View on GitHub
src/core.js

Summary

Maintainability
F
3 days
Test Coverage
/*
 * ylog
 * https://github.com/qiu8310/ylog
 *
 * Copyright (c) 2015 Zhonglei Qiu
 * Licensed under the MIT license.
 */

var EventEmitter = require('events').EventEmitter,
  os = require('os');

var chalk = require('chalk'),
  ms = require('ms');

var ns = require('./ns'),
  h = require('./helper');


var eve = new EventEmitter();
var ylogProtoKeys;


// 用于生成链式结构的 prototype
var ylogChainProto = {},
  ylogProto = require('./core-proto');


// bind event's emit, on, once function to ylogProto, and ylogChainProto
['emit', 'on', 'once'].forEach(function(key) {
  ylogProto[key] = eve[key].bind(eve);
  ylogChainProto[key] = eve[key].bind(eve);
});


// 短名字引用
var levels = ylogProto.levels,
  styles = ylogProto.styles,

  attributes = ylogProto.attributes,
  Tag = ylogProto.Tag;


function argsToStr(args)  { return ylogProto.format.apply(ylogProto, args); }
function write()          { ylogProto.output(argsToStr(arguments)); }
function writeln()        { ylogProto.output(argsToStr(arguments) + os.EOL); }
function md(str)          {
  return str.replace(ylogProto.markdownRegExp, function(raw, left, key, word) {
    var color = ylogProto.markdowns[key];
    if (!color) { return raw; }
    return left + ylogProto.brush(word, color);
  });
}

var randomBrushSeeds = {};
function randomBrush(text, seed)    {
  seed = seed || 'default';
  if (!(seed in randomBrushSeeds)) { randomBrushSeeds[seed] = 0; }
  var index = randomBrushSeeds[seed], len = ylogProto.colors.length;
  randomBrushSeeds[seed] = index + 1;
  return ylogProto.brush(text, ylogProto.colors[index % len]);
}


/**
 * 注入 flag , 使其可以链式调用
 * @param {String} flag
 */
function appendFlag(flag) {
  // Object.keys 不会输出用 defineProperty 定义的属性
  if (!ylogProtoKeys) { ylogProtoKeys = Object.keys(ylogProto).concat(['namespace', 'enabled']); }
  if (ylogProtoKeys.indexOf(flag) >= 0) {
    throw new Error('Flag <' + flag + '> is occupied by ylog prototype');
  }

  // 如果 flag 已经定义了,就不用再定义了,再定义也是一样,没区别
  if (flag in ylogProto) { return false; }

  var initYlog = function() {
    return chain([flag], {calledCount: 0, attributes: {}}, this);
  };

  var getProperty = function () {
    this.flags.push(flag);
    return chain(this.flags, this.options, this.ylog);
  };

  // Object.defineProperty(Ylog.prototype, flag, {get: initYlog});

  // 形式一:ylog.log 或 ylog('xx').log
  Object.defineProperty(ylogProto, flag, {get: initYlog});

  // 形式二:第 2+ 级的链式调用,如 ylog.log.write 或 ylog('xx').log.write, 它们在执行 write 的时候会触发此
  Object.defineProperty(ylogChainProto, flag, {get: getProperty});

}

// ylog generator
var ylogMap = {};
function makeYlog(namespace, enabled) {
  var cacheKey = namespace || 'default';
  if (cacheKey in ylogMap) { return ylogMap[cacheKey]; }

  // 没有指定 namespace 则表示默认使用 defaultYlog
  // 只备份这个 default,其它的 namespace 没必要备份,因为一般像 debug 一样,只会调用一次
  //if (!namespace && defaultYlog) { return defaultYlog; }

  var fn = function ylog(namespace) {
    return makeYlog(namespace, ns.enabled(namespace));
  };

  if (!namespace) {
    fn.namespace = '';
    fn.enabled = true;
  } else {
    // 添加颜色值
    fn.namespace = chalk.hasColor(namespace) ? namespace : randomBrush(namespace, 'ns');
    fn.enabled = enabled;
  }

  /* jshint ignore:start */
  fn.__proto__ = ylogProto;
  /* jshint ignore:end */

  ylogMap[cacheKey] = fn;

  return fn;
}

/**
 * 每次链式调用都重新生成一个函数,并且函数又支持链式调用
 * @param {Array} flags
 * @param {Object} options
 * @param {Object} ylog
 * @returns {Function}
 */
function chain(flags, options, ylog) {
  var caller = function ylogChain() {
    return call.apply(caller, arguments);
  };
  caller.flags = flags;
  caller.options = options;
  caller.ylog = ylog;
  // __proto__ is used because we must return a function, but there is
  // no way to create a function with a different prototype.

  /* jshint ignore:start */
  caller.__proto__ = ylogChainProto;
  /* jshint ignore:end */

  return caller;
}


var transforms = ylogProto.attributeTransforms;
function setAttribute(flag, args, options, batch) {
  var trans = transforms[flag];
  var val = args[0];
  switch (trans) {
    case Boolean:
      val = !!val;
      break;
    case Number:
      if (typeof val === 'boolean') {
        val = val ? 1 : 0;
        if (val && !batch) {
          val = getAttribute(flag, options) + val;
        }
      } else {
        val = parseInt(val, 10);
        val = isNaN(val) ? 1 : val;
      }
      break;
    case String:
      if (typeof val !== 'string') {
        return false;
      }
      break;
    default :
      if (typeof trans === 'function') {
        val = trans.apply(null, args);
      }
  }

  options.attributes[flag] = val;
}

function getAttribute(flag, options) {
  return ( flag in options.attributes ) ? options.attributes[flag] : ylogProto.attributes[flag];
}

/**
 * 分析 flags,返回其中的 levels 和 styles,并且设置上面的 attributes 到 options.attributes 中
 * @param {Array} flags
 * @param {Arguments} args
 * @param {Object} options
 */
function parseFlags(flags, args, options) {
  var i, flag, len = flags.length, no;
  var result = {levels: [], styles: []};
  for (i = 0; i < len; i++) {
    flag = flags[i];
    no = false;

    while (flag === 'no') {
      flag = flags[++i];
      no = !no;
    }

    if (flag.indexOf('no') === 0) {
      no = !no;
      flag = flag.substr(2);
    }

    if (flag === 'attr' || (flag in attributes)) {

      // attr 的参数必须要是个对象
      if (flag === 'attr') {
        /* jshint ignore:start */
        if (args.length && i === len - 1) {
          Object.keys(args[0]).forEach(function(key) {
            setAttribute(key, [args[0][key]], options, true);
          });
        }
        /* jshint ignore:end */
      } else {
        setAttribute(flag, args.length && i === len - 1 ? args : [!no], options);
      }
    }

    if (flag) {
      if (flag in levels) {
        result.levels.push(flag);
      } else if (flag in styles) {
        result.styles.push(flag);
      }
    }
  }

  return result;
}


function getFnResult(fn, args, ctx) {
  var result = fn.apply(ctx, args || []);
  return typeof result === 'undefined' ? '' : result;
}

var lastEOL = true; // 记录是否需要输出换行符(第一次输出不需要换行)
var lastTime;

/**
 * 函数形式的链式调用会执行此函数
 */
function call() {
  var flags = this.flags,
    options = this.options,
    ylog = this.ylog;

  var attr = function(flag) { return getAttribute(flag, options); };
  var rtn = chain([], options, ylog);

  // 如果 disabled, 则直接返回即可
  if (!ylog.enabled) { return rtn; }

  // 获取指定的 level 级别
  var parsed = parseFlags(flags, [].slice.call(arguments), options),
    flagLevels = parsed.levels,
    flagStyles = parsed.styles,
    flagLevel;

  // 如果上次使用了 level,则把它导入到此次的 call 中
  if (options.levels) { [].push.apply(flagLevels, options.levels); }
  flagLevel = getCallLevel(flagLevels);
  options.levels = flagLevels; // 保存当前 levels
  // 用户指定了 level,但没有用户要的 level;或者指定的 level 就是 silent
  if (flagLevels.length && !flagLevel || flagLevel === 'silent') { return rtn; }

  // 输出 ln 指定的换行
  var lines = attr('ln');
  if (lines > 0) {
    write(h.repeat(os.EOL, lines));
    if (options.labelLength) {
      write(h.repeat(' ', options.labelLength - 1));
    }
    delete options.attributes.ln;
  }

  var lastFlag = h.last(flags);

  // 最后一个是 exit()
  if (lastFlag === 'exit' && arguments[0] !== false) { process.exit.apply(process, arguments); }

  // 没有输出,也直接返回
  if (!flagLevels.length && !flagStyles.length) { return rtn; }

  // 根据 attributes 输出 styles
  var label = '', buffer = [], i, currTime;

  if (options.calledCount === 0) {
    if (!lastEOL) { writeln(); }
    currTime = Date.now();

    if (attr('tag')) {
      label = getLabel({
        pid: ylogProto.brush(process.pid, Tag.pid.color),
        ns: ylog.namespace,
        level: flagLevel && levels[flagLevel].tag
      });

      if (label) {
        if (ylog.namespace && Tag.ns.show) {
          label = h.repeat(attr('nsPadChar'), attr('nsPad')) + label;
        }
        label += ' ';
      }

      var labelArgs = attr('label');
      if (labelArgs && typeof labelArgs[0] === 'string') {
        label += h.align.apply(h, labelArgs) + ' ';
      }
    }

    label = h.repeat(attr('padChar'), attr('pad')) + label;

    options.label = label;
    options.labelLength = chalk.stripColor(label).length;


    write(label); // label 一定要输出来
  }

  // 最后一个 flag 肯定要单独处理
  if (lastFlag === h.last(flagStyles)) { flagStyles.pop(); }

  for (i = 0; i < flagStyles.length; i++) {
    buffer.push(getFnResult(styles[flagStyles[i]], [], options));
  }

  // 运行最后一个 flag,它是可能带有参数的
  if (lastFlag in levels) {
    buffer.push(argsToStr(arguments));
  } else if (lastFlag in styles) {
    buffer.push(getFnResult(styles[lastFlag], arguments, options));
  }

  // 没有东西可输出,直接返回
  if (!label && !buffer.length) { return rtn; }


  // 开始处理输出(如果上一个带有换行,下次输出就不用加空格了
  var output = '', reLastEOL = /\n$/, joinGlue = '';
  for (i = 0; i < buffer.length; i++) {
    output += joinGlue + buffer[i];
    joinGlue = reLastEOL.test(buffer[i]) ? '' : ' ';
  }

  // markdown
  if (attr('md')) { output = md(output); }

  var eol = attr('eol');
  output += h.repeat(os.EOL, eol - 1); // 留一个到最前面输出

  // 如果 attributes 中有设置了 no.eol,则表示不要输出换行符了
  lastEOL = eol < 1;

  // 设置颜色
  var color = attr('color');
  if (color === false) {
    output = chalk.stripColor(output);
  } else if (typeof color === 'string') {
    output = ylogProto.brush(output, color);
  }

  if (!options.calledCount) {
    if (attr('time')) {
      // time 标签不要放 label 中,因为 label 可能在第行都显示,而 time 只需要在第一次输出时显示即可
      output = getOutputTime(currTime) + ' ' + output;
    }
    lastTime = currTime;
  } else {
    // 在同一行上做第 2+ 次输出,要在前面加个空格(在设置了颜色之后,不想要这个 ' ' 也带有颜色)
    if (!options.ln) {
      output = ' ' + output;
    }
  }

  // wrap output
  var wrap = attr('wrap');
  output = ylogProto.computeWrap(wrap, output, options.labelLength);

  // prefix output
  if (options.label && output) {
    var prefix = attr('prefix');
    var outputPad = prefix ? options.label : h.repeat(' ', options.labelLength);

    // 上次输出了换行,则此行默认就得带上前缀
    output = ((options.ln ? outputPad : '') + output).replace(/\n/g, function(raw) {
      return raw + outputPad;
    });
  }

  eve.emit([chalk.stripColor(ylog.namespace) || '', flagLevel || ''].join('.'), output);

  write(output);

  options.calledCount++;
  options.ln = reLastEOL.test(output); // 判断最后一个字符是不是换行,如果是,第二次输出的时候不需要带 ' '
  return rtn;
}


function getOutputTime(currTime) {
  var diff = lastTime ? currTime - lastTime : 0;
  var levels = ylogProto.timeLevelColors;
  var color;
  for (var i = 0; i < levels.length; i++) {
    if (diff < levels[i][0]) {
      color = levels[i][1];
      break;
    }
  }
  return ylogProto.brush('[' + h.align(ms(diff), ylogProto.timeLabelLength - 2, 'center') + ']', color);
}


/**
 * 对 levels 排序的一个帮助函数
 * @param {String} a
 * @param {String} b
 * @returns {number}
 */
function sortLevels (a, b) { return levels[a].weight - levels[b].weight; }

/**
 * 从 levelFlags 中选出一个 level 来使用
 * @param {Array} levelFlags
 */
function getCallLevel(levelFlags) {
  levelFlags = [].concat(levelFlags);

  if (!ylogProto.level || !levelFlags.length) {
    return levelFlags.sort(sortLevels).pop(); // 返回一个权重最最的即可
  }

  var userLevels = [].concat(ylogProto.level), // clone 一份,防止修改了原数据,同时将其转化成数组
    level;

  if (ylogProto.levelMode === 'only') {
    // 从 userLevels 中选一个存在 levelFlags 中的权重最高的 level
    userLevels.forEach(function(flag) {
      if (levelFlags.indexOf(flag) >= 0) {
        if (!level || levels[flag].weight > level.weight) {
          level = flag;
        }
      }
    });
  } else {

    // 从 userLevels 中选一个权重最低的,然后再在 levelFlags 中选一个比 userLevels 中权重最低的要高的一个最高的 level
    var min = userLevels.sort(sortLevels).shift();
    var max = levelFlags.sort(sortLevels).pop();
    if (levels[min].weight <= levels[max].weight) {
      level = max;
    }
  }
  return level;
}

function getLabel(opts) {
  return Object.keys(Tag)
    .filter(function(key) { return Tag[key].show && opts[key]; })
    .sort(function(k1, k2) { return Tag[k1].order - Tag[k2].order; })
    .map(function(key) {
      var cfg = Tag[key];
      return h.align(opts[key], (cfg.len < 0 && cfg.max ? cfg.max : cfg.len), cfg.align, cfg.fill);
    }).join(' ');
}


/**
 * 添加或修改 Level Flag
 *
 * @param {String} name - 级别名称
 * @param {Number} weight - 级别权重,越小优先级越高
 * @param {String} tag - 指定最左边显示的标识(如果没有,则使用级别的名称,即 name)
 */
ylogProto.levelFlag = function(name, weight, tag) {
  tag = tag || name;

  var tagLen = chalk.stripColor(tag).length;
  if (!Tag.level.max || tagLen > Tag.level.max) {
    Tag.level.max = tagLen;
  }

  levels[name] = {
    weight: weight,
    tag: tag
  };

  appendFlag(name);
};


/**
 * 添加或修改 Style Flag
 *
 * @param {String} name - 样式名称
 * @param {Function} fn - 样式处理程序,fn 绑定在了 chalk 之上,所以你可以在 fn 中使用 this.red.bgGreen 等 chalk 方法
 */
ylogProto.styleFlag = function(name, fn) {
  styles[name] = fn;
  appendFlag(name);
};



// 注入属性 flag
appendFlag('no');
appendFlag('exit');
appendFlag('attr');
Object.keys(attributes).forEach(function(key) {
  appendFlag(key);
  appendFlag('no' + key);
});


// 总是在程序最后输出一个换行符
process.on('exit', function() { writeln(); });


module.exports = makeYlog();