snowyu/terminal-logger.js

View on GitHub
src/index.coffee

Summary

Maintainability
Test Coverage
inherits        = require 'inherits-ex/lib/inherits'
createObject    = require 'inherits-ex/lib/createObjectWith'
getPrototypeOf  = require 'inherits-ex/lib/getPrototypeOf'
format          = require 'util-ex/lib/format'
isArray         = require 'util-ex/lib/is/type/array'
isObject        = require 'util-ex/lib/is/type/object'
isString        = require 'util-ex/lib/is/type/string'
defineProperty  = require 'util-ex/lib/defineProperty'
Table           = require 'cli-table'
colors          = require 'colors/safe'
Logger          = require 'abstract-logger'
getKeys         = Object.keys

textColor = (aColors, aText)->
  if isString aColors
    aColors = [aColors]
  if isArray aColors
    aColors.forEach (color)->
      aText = colors[color] aText
  aText

# `Logger` is a [logref](https://github.com/mikeal/logref)
# compatible logger, with an enhanced API.
#
# All logs are done against STDERR, letting you stdout for meaningfull
# value and redirection, should you need to generate output this way.
#
module.exports = class TerminalLogger
  inherits TerminalLogger, Logger

  constructor: (aName, aOptions)->
    return createObject TerminalLogger, arguments unless @ instanceof TerminalLogger
    super
    aOptions = aName if isObject aName
    if isObject aOptions
      @colors = aOptions.colors if aOptions.colors
      @statusLevels = aOptions.statusLevels if aOptions.statusLevels
    @colors ?= stColors

  # padding step
  step: '  '
  padding: ' '

  pad = (status, max) ->
    max ?= 'identical'.length
    delta = max - status.length
    (if delta then new Array(delta + 1).join(' ') + status else status)

  noBorderTable: noBorderTable =
    'top': ''
    'top-mid': ''
    'top-left': ''
    'top-right': ''
    'bottom': ''
    'bottom-mid': ''
    'bottom-left': ''
    'bottom-right': ''
    'left': ''
    'left-mid': ''
    'mid': ''
    'mid-mid': ''
    'right': ''
    'right-mid': ''
    'middle': ' '
  noPaddingTable: noPaddingTable =
    'padding-left': 0
    'padding-right': 0

  # color -> status mappings
  stColors =
    skip: 'magenta'
    force: 'yellow'
    create: 'green'
    invoke: 'bold'
    conflict: 'red'
    identical: 'cyan'
    ok: 'green'
    emergency: ['red', 'bold']
    alert: 'red'
    critical: 'red'
    error: 'red'
    warning: 'yellow'
    notice: 'gray'
    info: 'gray'
    debug: 'blue'
    trace: 'blue'
    '✔': 'green'
    '✗': 'red'
    name: 'blue'

  # status -> level mappings
  statusLevels: stLevels =
    skip: 'warning'
    force: 'warning'
    create: 'notice'
    invoke: 'notice'
    conflict: 'error'
    identical: 'error'
    ok: 'notice'
    '✔': 'notice'
    '✗': 'error'

  _colorProp: (aObject, aName, aDefaultValue)->
    s = aObject[aName]
    aObject[aName] = textColor(@colors[s]||@colors[aName]||aDefaultValue, s) if s?
    return

  formatter: (aContext, args...) ->
    @_colorProp(aContext, 'status')
    @_colorProp(aContext, 'level')
    aContext.name = @name if !aContext.name? and @name?
    @_colorProp(aContext, 'name', 'blue')
    super

  inLevelContext: (aContext)->
    vStatus = aContext.status
    if vStatus? and !aContext.level?
      vLevel = @statusLevels[vStatus]
      vLevel ?= vStatus
      aContext.level = vLevel if @levelStr2Id(vLevel)?
    result = super aContext
    result

  defineProperty @::, '_colors'
  defineProperty @::, '_maxStatus' # the max length status in colors
  defineProperty @::, 'status'
  defineProperty @::, 'colors', undefined,
    get: -> @_colors
    set: (value)-> @updateColors(value)

  # return the max len of status in the aColors if successful.
  getMaxLenInColors: (aColors)->
    aColors ?= @_colors
    result = 0
    getKeys(aColors).forEach (status) ->
      result = status.length if status.length > result
    result

  _clearStatus: (aColors)->
    aColors ?= @_colors
    @status = (aStatus, args...)->
      aStatus = aStatus.toLowerCase()
      vLevel = @statusLevels[aStatus]
      vLevel ?= aStatus
      vLevel = @levelStr2Id(vLevel)
      if !vLevel? or @inLevel vLevel
        vColor = aColors[aStatus] if aColors
        vStr = format.apply(null, args)
        vLN = @NEWLINE
        padding = @padding
        if vLN
          vStr = vStr.split(vLN).map (s)->
            result = if s then padding + s else s
          .join(vLN)
        vStr = @table
          chars: noBorderTable,
          style: noPaddingTable,
          colAligns:['right', 'left']
          rows: [[textColor(vColor, pad(aStatus, @_maxStatus)), vStr]]
        vStr += @NEWLINE
        @write vStr
      return this
    # End @status
    @

  updateColors: (aColors)->
    if isObject(aColors)
      @_colors = aColors
      @_maxStatus = @getMaxLenInColors(aColors)

      @_clearStatus(aColors)
      that = @
      # maybe I should deprecate these:
      # Only reserve the status() method.
      getKeys(aColors).forEach (status)->
        # Each predefined status has its logging method utility, handling
        # status color and padding before the usual `.write()`
        #
        # Example
        #
        #    log
        #      .write()
        #      .status.info('Doing something')
        #      .status.force('Forcing filepath %s, 'some path')
        #      .status.conflict('on %s' 'model.js')
        #      .write()
        #      .ok('This is ok');
        # Returns the logger
        if status isnt 'name'
          that.status[status] = (args...)->
            args.unshift status
            that.status.apply that, args
    @

  # Write a string to stderr.
  #
  # Returns the logger
  _write: (msg)->
    process.stderr.write msg
    @

  # Convenience helper to write sucess status, this simply prepends the
  # message with a green `✔`.
  tick: ->
    vText = '✔'
    vColor = @colors[vText] if @colors
    vColor ?= 'green'
    @write @padding + textColor(vColor, vText+' ') + format.apply(null, arguments) + @NEWLINE
    this

  # Convenience helper to write error status, this simply prepends the
  # message with a red `✗`.
  cross: ->
    vText = '✗'
    vColor = @colors[vText] if @colors
    vColor ?= 'red'
    @write @padding + textColor(vColor, vText+' ') + format.apply(null, arguments) + @NEWLINE
    this

  up: ->
    @padding += @step
    this

  down: ->
    @padding = @padding.replace(@step, '')
    this

  # A basic wrapper around `cli-table` package, resetting any single
  # char to empty strings, this is used for aligning options and
  # arguments without too much Math on our side.
  #
  # - aOptions - A list of rows or an options object to pass through cli
  #              table.
  #
  # Returns the table reprensetation string.
  table: (aOptions) ->
    if isArray(aOptions)
      vRows = aOptions
      aOptions = null
    else if aOptions
      vRows = aOptions.rows
    result = new Table aOptions
    if isArray vRows
      vRows.forEach (row)->result.push row
    result.toString()

  (->
    for k, id of Logger::levels
      continue if id < 0
      k = k.toLowerCase()
      TerminalLogger::[k] = ((k)->
        return ->
          @status[k].apply @, arguments
      )(k)
  )()