console2
Version:
Improved console: object inspection, stack traces, tables with ASCII box-drawing characters and colors.
1,674 lines (1,437 loc) • 43.8 kB
JavaScript
// set encoding to utf8
process.stdout.setEncoding('utf8')
const async = require('async')
const colors = require('chalk')
const pkg = require('./package')
class Log {
/**
* Log boxes
*
* @param {*} parent - a parent box or a line
* @param opt
* @constructor
*/
constructor(parent, opt){
// generate id (time+rand)
this.id = (new Date().getTime()).toString(36)+((Math.random().toString(36).substr(2, 5)))
this.lines = []
this.parent = null
this.level = 0
this.printedLines = 0
// this.colors = colors
// make utils accessible
this.col = Log.col
this.strip = Log.strip
this.pad = Log.pad
// set color to colorText by default
if(typeof opt === 'object' && opt.color && !opt.colorText)
opt.colorText = opt.color
// default options
this.opt = Object.assign({
console: null, // object to receive the output of console2
border: typeof opt === 'number' ? opt : 1, // vertical border width (1 or 2)
color: typeof opt === 'string' ? opt : 'grey', // border color
colorText: typeof opt === 'string' ? opt : 'grey', // text color
isWorker: false, // run as a worker
map: [['...','…']], // auto replace
enableAutoOut: false, // enable auto out calls (used when in node console)
disableWelcome: false, // disable our kind welcome line
override: false, // override nodes console
animate: false, // animate idle status
over: false // box status
}, opt||{})
// is direct log
if(this._instanceof(parent)){
this.parent = parent
this.level = parent && parent.level ? parent.level+1 : 1
// inherit console object when not given
if(!this.opt.console)
this.opt.console = this.parent.opt.console
}
// use default
if(!this.opt.console)
this.opt.console = Log.console
this.timer = {
_: new Date().getTime(),
_calls: {}
}
if(this.level === 0){
// animate
if(this.opt.animate){
process.on('SIGINT', () => {
Log.clearLine()
process.stdout.write(Log.col('┘', this.opt.color)+"\n")
process.exit()
})
// passive set interval
setInterval(this._animate.bind(this), 500).unref()
}
}
}
//─── Static methods ──────────────────────────────────────────────────
/**
* Color[color] shortcut
*
* @param {String} str - the text
* @param {...String} cmd - the color or action (red, bold...)
* @returns {*}
*/
static col(str, cmd){
const cmds = Array.prototype.slice.call(arguments, 1)
// convert 2 str
if(typeof str != 'string')
str += ''
let res = str
switch(cmd){
// rainbow
// using the first 5 colors of Log.chalkColors
case 'rainbow':
const cols = Log.chalkColors.slice(0, 5)
return str.split('').map(function(char, i){
return colors[cols[i % cols.length]](char)
}).join('')
// zebra
// using white, then bgWhite & black
case 'zebra':
return str.split('').map(function(char, i){
return i % 2 ? colors.white(char) : colors.bgWhite.black.dim(char)
}).join('')
case 'code':
return colors.bgBlue.white(str.split('').map(function(char){
if('.,:;=()[]{}+-*|"/\''.indexOf(char) > -1)
return colors.grey(char)
return char
}).join(''))
}
// add attributes
cmds.forEach(function(cmd){
res = colors[cmd](res)
})
return res
}
/**
* Clear terminal line + set cursor to 0
*/
static clearLine(){
process.stdout.clearLine()
process.stdout.cursorTo(0)
return process.stdout
}
/**
* Truncate string
*
* @param {String} str
* @param {Number} length
* @param {String} [postfix]
* @returns {String}
*/
static truncate(str, length, postfix){
// convert 2 string
str = str+''
const plain = Log.strip(str)
// no action required
if(plain.length <= length)
return str
// default postfix
if(!postfix) postfix = '…'
// subtract postfix from length
length -= postfix.length
// return empty
if(length < 0)
return ''
return str.substring(str, length) + postfix
}
/**
* Uppercase first character
* @param {String} str
* @returns {String}
*/
static capitalize(str){
return str.substr(0,1).toUpperCase()+str.substr(1)
}
/**
* Pad a str
*
* Use in three ways:
* .pad('-', 5) = '-----'
* .pad('.', 7, 'Hello') = 'Hello..'
* .pad(' ', 7, 'Hello', true) = ' Hello'
*
* @param {String} padSymbol
* @param {Number} length
* @param {String} [str]
* @param {Boolean} [useLeftSide]
* @returns {String}
*/
static pad(padSymbol, length, str, useLeftSide){
let out = ''
if(str){
while(str.length < length){
if(useLeftSide)
str = padSymbol + str
else
str += padSymbol
}
return str
}
for(let i = 0; i < length; i += padSymbol.length){
if(useLeftSide)
out = padSymbol + out
else
out += padSymbol
}
return out
}
/**
* Format str in printf style as console does
* Slightly edited version of the original node utils.format
*
* @param f
* @returns {*}
*/
static format(f) {
const args = Array.prototype.slice.call(arguments)
if(args.length === 1) return args
let i = 1
const len = args.length
const str = String(f).replace(/%[sdj%]/g, function(x){
if(x === '%%') return '%'
if(i >= len) return x
switch (x) {
case '%s': return String(args[i++])
case '%d': return Number(args[i++])
case '%j':
try {
return JSON.stringify(args[i++])
} catch (_) {
return '[Circular]'
}
// falls through
default:
return x
}
})
// formatting happened
if(str !== args[0]){
// use result
args[0] = str
// remove formatting argument
args.splice(1, 1)
}
return args
}
/**
* Check if script is run from terminal
*
* @returns {boolean}
*/
static isTerminalConsole(){
return module && module.parent && (module.parent.id+'') === 'repl'
}
/**
* Parse keywords / add colors
*
* @param word
* @returns {String}
* @private
*/
static parseWord(word){
// basic types
if(word === null)
word = colors.grey.italic('null')
else if(word === undefined)
word = colors.red('undefined')
else if(word === true)
word = colors.green.bold('true')
else if(word === false)
word = colors.bold.red('false')
else if(word === pkg.name){
word = Log.col(word, 'rainbow')
}
else if(typeof word == 'number')
word = colors.cyan(word+'')
else {
let s = word.toString()
switch(s){
default: s = null
}
if(s)
word = s
}
return word
}
/**
* Wrap text
*
* @param {String} str
* @param {Number} length
* @returns {*}
*/
static wrap(str, length){
const lineWrap = require('linewrap')
return lineWrap(length, {
skipScheme:'ansi-color',
respectLineBreaks: 'multi',
tabWidth: 3
})(str)
}
/**
* Get stdout columns
*
* @returns {number}
* @private
*/
static getTerminalWidth(){
return process.stdout.columns-1 || 100
}
/**
* Pluralize a string, poor mans function
*
* @param nr
* @param str
* @returns {string}
*/
static plural(nr, str){
const i = parseFloat(nr)
nr = typeof nr == 'number' ? nr.toFixed(0) : nr
return str.replace('%s', Log.col(nr, 'cyan')) + (i !== 1 ? 's' : '')
}
// clone console
static console = Object.assign({}, console)
// color names
static chalkColors = ['cyan','green','yellow','red','magenta','blue','white','grey','black']
static chalkCommands = ['reset','bold','dim','italic','underline','inverse','hidden','strikethrough']
// shortcuts
static strip = colors.stripColor
//─── Dynamic methods ──────────────────────────────────────────────────
/**
* Override the console with this
*
* @returns {Log}
*/
overrideConsole(){
// im a dirty little whore
if(console instanceof Log)
return this.warn('Console already overwritten')
if(Log.isTerminalConsole()){
// disable 'undefined' console messages
process.stdin.emit('data', 'module.exports.repl.ignoreUndefined = true;\n')
}
// copy original console
this.opt.console = Log.console
// override console
console = Object.assign(console, this)
// finish
return this
}
/**
* Set options
*
* @param {String|Number|Object} opt
* @returns {Log}
*/
options(opt){
if(opt === undefined)
return this
// handle options('string') - color and 'ready'
if(typeof opt == 'string'){
opt = {color:opt}
}
// handle options(2) - border width
else if(opt === 1 || opt === 2){
opt = {border:opt}
}
// use color as colorText when colorText not given
if(opt.color && !opt.colorText)
opt.colorText = opt.color
// set options
this.opt = Object.assign(this.opt, opt)
return this
}
/**
* Create a new box
*
* @param [line]
* @param [opt]
* @returns {Log}
*/
box(line, opt){
if(arguments.length === 1){
// line could be either an option or a line
if(typeof line === 'string'){
// check if line is color
if(Log.chalkColors.indexOf(line) > -1 || Log.chalkCommands.indexOf(line) > -1){
opt = {color:line}
}
}
// line is no string and therefore an option
else {
opt = line
line = null
}
}
// create box
const box = new Log(this, opt||{})
// auto add 1st line when given
if(line){
box.line(line)
}
// add box to myself
this.line(box)
return box
}
/**
* Display help
*/
help(){
require('./help')
}
/**
* Save a line to buffer
*
* @param {String|Object} line
* @param {...String|Object} [option]
* @returns {Log}
*/
line(line, option){
let args = Array.prototype.slice.call(arguments)
const obj = {
prefix: ' ',
color: this.opt.color,
colorText: this.opt.colorText
}
// handle .line()
if(line === undefined){
args[0] = undefined
}
// try to find an option
option = (args[args.length-1]+'')
// prefix
if(option.substr(0,4) === 'pre:'){
args.pop()
obj.prefix = option.substr(4)
}
// color
else if(Log.chalkColors.indexOf(option) > -1 || Log.chalkCommands.indexOf(option) > -1){
args.pop()
obj.color = option
obj.colorText = option
}
// use format (sprintf) like console does
args = Log.format.apply(this, args)
// handle section
const processStash = () => {
// empty line (undefined,'')
if(!stash.length && line === undefined){
this.lines.push(obj)
}
// join word string
else if(stash.length){
obj.line = stash.join(' ')
// replace map (... > …)
if(Array.isArray(this.opt.map)){
this.opt.map.forEach(function(arr){
obj.line = obj.line.replace(arr[0],arr[1])
})
}
this.lines.push(obj)
}
// log
else if(this._instanceof(line)){
this.lines.push(line)
}
// empty stash
stash = []
}
// iterate args
let stash = []
args.forEach(function(word){
// types to strings
word = Log.parseWord(word)
// is str
if(typeof word == 'string'){
stash.push(word)
}
// object
else {
// skip sub boxes
if(this._instanceof(word))
return
// add to this.lines
processStash()
// log obj
this._try(this._object, word)
}
}.bind(this))
// add
processStash()
return this
}
/**
* Alias for this.line
*
* @returns {Log}
*/
_(...args){
return this.line.apply(this, args)
}
/**
* Alias for this.line
*
* @returns {Log}
*/
log(...args){
this.line.apply(this, args)
return this.out()
}
/**
* info - Alias for this.log in "green"
*
* @returns {Log}
*/
info(...args){
args.push('green')
this.line.apply(this, args)
this.out('info')
return this
}
/**
* ok - Shortcut to indicate sth went alright
*
* @returns {Log}
*/
ok(){
this.time('_').out()
return this
}
/**
* Alias for this.log
*
* @returns {Log}
*/
dir(...arg){
return this.log(...arg)
}
/**
* Alias for this.line in "red"
*
* @returns {Log}
*/
error(...args){
// add red
args.push('red')
// log
if(this.line){
this.line.apply(this, args)
this.out('error')
}
return this
}
/**
* Alias for this.line in "yellow"
*
* @returns {Log}
*/
warn(...args){
// add yellow
args.push('yellow')
// log
this.line.apply(this, args)
this.out('warn')
return this
}
/**
* Output time
*
* Use as:
* .time() - Prints time since box was initialized
* .time('TimerName') (1st call) - starts a timer for tony, outputs 'TimerName: start'
* .time('TimerName', true) (1st call) - same as above, no output
* .time('TimerName') (2nd call) - outputs 'TimerName: Xms'
* .time('TimerName', true) (2nd call) - outputs 'TimerName: Xms - reset', resets the timer
*
* @param {String} [label]
* @param {Boolean} [reset]
* @returns {Log}
*/
time(label, reset){
const now = new Date().getTime()
// initialize timer
if(label && !this.timer[label]){
this.timer[label] = now
// finish quietly
if(reset)
return this._autoOut()
// indicate event
this.line(Log.col(label, 'green')+': start')
return this._autoOut()
}
// calc
let passed = (now - (this.timer[label || '_']))
let lastExec = this.timer._calls[label || '_']
// build line
let line = Log.col(label === '_'?'OK':label || 'Time',passed<=10?'green':(passed<=100?'yellow':'red'))+Log.col(': ', 'grey')
+ Log.col(passed.toFixed(0)+'ms', label === '_' ? 'grey' : 'white')
+ (reset ? Log.col(' - ', 'grey') + Log.col('reset', 'yellow'):'')
// add "+Xms"
if(lastExec){
lastExec = (now - lastExec)
let str = lastExec.toFixed(0)
str = ' '+Log.col(Log.pad('─', (Log.getTerminalWidth()-(this.level+2)-Log.strip(line).length-str.length) - 7), 'grey')
+ ' +'+lastExec.toFixed(0)+'ms'
if(lastExec <= 10)
str = Log.col(str, 'green')
if(lastExec <= 100)
str = Log.col(str, 'yellow')
if(lastExec > 100)
str = Log.col(str, 'red')
line += str
}
// reset timer
if(label && this.timer[label] && reset){
this.timer[label] = now
}
// output time passed
this.line(line)
// save call time
this.timer._calls[label||'_'] = now
if(passed > 10000){
const res = []
const struc = {
year: 31536000,
month: 2592000,
day: 86400,
hour: 3600,
minute: 60,
second: 1
}
let delta = passed / 1000
// calc time
Object.keys(struc).forEach(function(key){
const r = Math.floor(delta / struc[key])
struc[key+'s'] = r
delta -= r * struc[key]
if(r > 0 || res.length > 0)
res.push(Log.plural(r, '%s '+key))
})
this.box(Log.col(res.join(' + '), 'grey')).over()
}
// output?
return this._autoOut()
}
/**
* Alias for this.time
*
* @returns {Log}
*/
timeEnd(...args){
return this.time(...args)
}
/**
* trace - beautified
*
* @param {String} [message]
*/
trace(message){
const obj = {}
Error.captureStackTrace(obj, this)
const lines = []
obj.stack.split("\n").forEach(function(line, indexLine){
// remove surrounding whitespaces
line = line.trim()
const words = []
// 1st line
if(indexLine === 0){
return lines.push(colors.yellow(message||'Trace')+colors.grey(': ')+colors.cyan(line))
}
// skip trace to this place
else if(indexLine === 1){
}
// show rest of stack
else {
// split to words
line.split(' ').forEach(function(word, indexWord){
switch(indexWord){
case 0:
//words.push(colors.grey(word))
break
case 1:
words.push(colors.white(word))
break
case 2:
words.push(colors.grey(word))
break
}
})
lines.push(words.join(' '))
}
})
let box
lines.forEach(function(line, i){
if(i === 0)
return box = this.line(line)
box = box.box(line).over()
}.bind(this))
if(box)
box._autoOut()
return this
}
/**
* Display text inside a box
*
* @param line
* @returns {Log}
*/
title(line){
const args = Array.prototype.slice.call(arguments)
const maxWidth = Log.getTerminalWidth()
// top border
this.line(Log.col(Log.pad('─', maxWidth - this.level - 2)+'┐', this.opt.color, 'dim'), 'pre:')
// build line
this.line.apply(this, args)
// get inserted line
const newLine = this.lines[this.lines.length-1]
// add right border
newLine.line += Log.pad(' ', maxWidth - this.level - Log.strip(newLine.line).length - 3)
+ Log.col('│', this.opt.color, 'dim')
// save to this.lines
this.lines[this.lines.length-1] = newLine
// bottom border
this.line(Log.col((Log.pad('─', maxWidth - this.level - 2)+'┘'), this.opt.color, 'dim'), 'pre:')
// use out?
return this
}
/**
* End the current line and insert an empty line (uses this.out!)
*/
spacer(){
// end line
this.out()
// empty line
this.opt.console.log(Log.col('┘', this.opt.col||'grey'))
this.printedLines = 0
return this
}
/**
* beep sound
*
* @returns {*}
*/
beep(label){
process.stdout.write('\x07')
return this.line(Log.col('BEEP'+(typeof label == 'string'?': '+label:''), 'red'))//.out()
}
/**
* Build output string
*
* @param {Function} callback
* @param {Boolean} [preserveLines=false]
* @returns {string}
*/
_buildString(callback, preserveLines){
const lines = []
const maxWidth = Log.getTerminalWidth()
let body = ''
let allNr = 0
let mapBase = this
// gather lines
const walk = (log, callbackWalk) => {
let boxNr = 0
// iterate lines
async.each(log.lines, function(line, callbackLines){
// sub box
if(this._instanceof(line)){
return line.opt.over ? walk(line, callbackLines) : callbackLines()
}
// format
lines.push({
id: log.id,
level: log.level,
boxNr: boxNr,
allNr: allNr,
prefix: line.prefix,
color: line.color,
colorText: line.colorText,
line: line.line,
log: log
})
// remove printed lines from stack
if(!preserveLines){
// Log.console.log('REMOVE', log.lines)
log.lines = log.lines.filter(function(l){
if(log._instanceof(l)) return l.id !== log.id
return l !== line
})
// this.lines = this.lines.filter(function(l){
// if(this._instanceof(l)) return l.id != log.id
// return l.line == line.line// || l.id == log.id
// }.bind(this))
}
// count
boxNr++
allNr++
// end
callbackLines()
}.bind(this), () => callbackWalk())
}
// prepare
walk(this, () => {
// iterate
async.each(lines, (obj, callbackLine) => {
const i = lines.indexOf(obj)
// count total output
this.printedLines++
// structure
const pre = {
str: '',
plain: ''
}
// shortcuts
obj.levelPrev = lines[i-1] ? lines[i-1].level : null
obj.levelNext = lines[i+1] ? lines[i+1].level : null
obj.hasPrev = lines[i-1] || false
obj.hasNext = lines[i+1] || false
obj.hasBoxPrev = obj.hasPrev && obj.hasPrev.id === obj.id
obj.hasBoxNext = obj.hasNext && obj.hasNext.id === obj.id
// iterate level times (|)
for(let posLeft = 0; posLeft <= obj.level; posLeft++){
let s
// set base obj
mapBase = obj.log.getParent(obj.level-posLeft)
// 1st from right
if(posLeft === obj.level){
if(this.printedLines === 1 && obj.level === 0)
s = '┌'
else if(obj.hasNext && obj.level === 0){
if(Log.strip(obj.line) === 'undefined')
s = '│'
else
s = '├'//┌
}
else if(obj.boxNr === 0){
if(obj.hasBoxNext || obj.levelNext > obj.level)
s = '┬'//┬
else if(obj.hasBoxPrev)
s = '└'//└
else if(obj.level === 0){
if(obj.line.substr(0,2) === ' ')
s = '│'
else
s = '├'
}
else
s = '─'//─
}
else if(obj.hasNext && obj.levelNext >= obj.level && Log.strip(obj.line) === 'undefined')
s = '│'
else if(
obj.hasNext
&& (
obj.hasNext.log.id === obj.log.id
|| (
obj.hasNext.log
&& obj.hasNext.log.parent
&& obj.hasNext.log.parent.id === obj.log.id
)
)
)
s = '├'
else if(['═','╛'].indexOf(obj.prefix.substr(0,1)) > -1)
s = '╘'
else if(obj.level === 0)
s = '├'
else
s = '└'
}
// 2nd from right when first of box
else if(obj.boxNr === 0 && posLeft === obj.level-1){
if(!obj.hasPrev)
s = '┬'//┌├
else if(!obj.hasNext || obj.hasNext && obj.hasNext.log.level < obj.level-1)
s = '┴'
else {
s = '├'
}
}
// 2nd from right when no next (is closing)
else if(!obj.hasBoxNext && obj.levelNext < obj.level-1 && posLeft === obj.level-1)
s = '┘'//┘
else if(!obj.hasBoxNext && obj.levelNext < obj.level-1){
if(!obj.hasNext){
if(posLeft === 0)
s = '├'//└
else if(posLeft < obj.level)
s = '┴'
else
s = '└'
}
else {
if(posLeft === obj.levelNext)
s = '├'
else if(posLeft < obj.levelNext)
s = '│'
else if(posLeft === 0 || posLeft === obj.levelNext)
s = '├'
else{
s = '┴'
}
}
}
// any other char
else {
// if(posLeft === 0 && !obj.hasNext)
// s = '┘'
// else
if(posLeft === 0 && !obj.hasPrev)
s = '├'
else
s = '│'
}
if(s){
pre.plain += s
pre.str += mapBase._map(s)
}
}
// add final prefix (customizable)
pre.plain += obj.prefix
pre.str += Log.col(obj.prefix, obj.log.opt.color)
// create str
const str = {
str: obj.line
}
// add plain str
str.plain = Log.strip(str.str)
switch(str.plain){
case 'undefined':
str.str = ''
break
case '---':
str.str = ''
const width = Log.getTerminalWidth() - obj.level - 1
for(let iLine = 0; iLine < width; iLine++){
str.str += '─'
}
pre.str = pre.str.substr(0,pre.str.length-6)
pre.plain = pre.plain.substr(0,pre.plain.length-1)
str.str = Log.col(str.str, obj.color, 'dim') //+'┤')
break
}
// truncate
if((pre.plain.length + str.plain.length) > maxWidth || str.plain.indexOf('\n') > -1){
// generate new lines
const addLines = Log.wrap(str.str, maxWidth - obj.level-10).split('\n')
str.str = ''
// iterate new lines
addLines.forEach((line, posTop) => {
const isTopLast = posTop === addLines.length - 1
line = ' '+Log.col(line, obj.color, 'bold')
// iterate prefix chars
pre.str = ''
pre.plain.split('').slice(0, -1).forEach((char, posLeft, all) => {
const isLeftLast = posLeft === all.length-1
let s
if(isLeftLast && posTop === 0){
if(char === '└')
s = '├'
else if(char === '├')
s = '├'
else{
s = '┌'
}
}
else if(isLeftLast && !isTopLast)
s = '│'
else if(isLeftLast && isTopLast){
if(obj.hasNext && (obj.hasNext.level === obj.level+1 || obj.hasNext.level === obj.level))
s = '│'
else if(obj.level === 0){
s = '├'
}
else
s = '└'
}
else if(posTop && ['┴','┘'].indexOf(char) > -1)
s = ' '
else if(!posLeft && posTop)
s = '│'
else if(char === '├' && posTop !== 0)
s = '│'
else
s = char
if(s)
pre.str += this._map(s)
})
// add truncated
body += "\n" + Log.col(pre.str, obj.color) + Log.col(line, obj.colorText)
})
}
else {
// add without truncation
body += "\n" + pre.str + Log.col(str.str, obj.colorText, 'bold')
}
callbackLine()
},
// bind callback so the garbage collector doesn't eat it
function(){ arguments[0](body) }.bind(this, callback))
})
}
/**
* Mark a sub box as ready for output.
*
* @returns {undefined|Log}
*/
over(){
const args = Array.prototype.slice.call(arguments)
// mimic .line
if(args.length)
this.line.apply(this, args)
// set ready
this.opt.over = true
// end
return this._return()
}
/**
* Output box string
*
* @param {String} [method="log"]
* @returns {*}
*/
out(method){
if(!method) method = 'log'
// mark as ready to print
this.opt.over = true
// enable .line args
if(method && Object.keys(Log.console).indexOf(method) < 0){
const args = Array.prototype.slice.call(arguments)
this.line.apply(this, args)
}
// use parent out when available
if(this.parent){
return this.parent.out()
}
// try to build
this._try(
this._buildString,
str => {
str = str.substr(1)
// the output
if(str){
Log.clearLine()
process.stdout.write(str+'\n')
}
// clear lines
// this.lines = []
}
)
return this._return()
}
/**
* Return buffer as string
* @returns {Promise<any>}
*/
build(stripLevels, useParent){
return new Promise((res) => {
// mark as ready to print
this.opt.over = true;
// use parent out when available
(useParent ? (this.parent || this) : this)._buildString(function(str){
str = str.substr(1)
if(stripLevels > 0){
str = Log.strip(str)
str = str.replace(new RegExp('^.{'+stripLevels+'}(.+\n?)$', 'gm'), '$1')
}
return res(str)
}, true)
})
}
/**
* Return a specific parent for _buildString structure
*
* @param [generations=1]
*/
getParent(generations){
let log = this
while(generations > 0){
if(!log.parent)
return generations = 0
log = log.parent
generations--
}
return log
}
_animate(){
Log.clearLine()
if(this.level || !this.opt.animate)
return
const animation = ['└','┘']
if(!this._animation || this._animation > animation.length-1)
this._animation = 0
process.stdout.write(this.col(animation[this._animation], this.opt.color))
this._animation++
}
/**
* Make sure promised are not used when on node console
*
* @returns {undefined|Log}
* @private
*/
_return(){
// don't use promise on node console
return Log.isTerminalConsole() ? undefined : this
}
/**
* Try-catch a function with args
*
* @param {Function} funct
* @param {...*} arg
* @private
*/
_try(funct, arg){
const args = Array.prototype.slice.call(arguments)
args.shift()
try {
funct.apply(this, args)
} catch(error) {
if(error instanceof RangeError){
this
.line('RangeError: '+error.message, 'red')
.line('Fallback to native console:').out()
Log.console.log("\n", arg)
}
}
}
/**
* Determine whether instance is an instance of Log or console.Console (if override happened)
*
* @param {*} instance
* @returns {boolean}
* @private
*/
_instanceof(instance){
return instance ? instance instanceof Log || instance instanceof Log.console.Console : false
}
/**
* Use out when there is no parent
*
* @returns {Log}
* @private
*/
_autoOut(){
return this.parent === null && this.opt.enableAutoOut === true ? this.out() : this
}
// ─── Dynamic private methods ──────────────────────────────────────────────────
/**
* Map symbols
*
* @param sym
* @returns {*}
* @private
*/
_map(sym){
const opt = this.opt
if(opt.border > 1){
switch(sym){
case '╘': sym = '╚'; break
case '┌': sym = '╓'; break
case '┘': sym = '╜'; break
case '│': sym = '║'; break
case '├': sym = '╟'; break
case '└': sym = '╙'; break
case '┬': sym = '╥'; break
case '┼': sym = '╫'; break
case '┴': sym = '╨'; break
}
}
if(opt.color){
sym = colors[opt.color||'white'](sym)
} else
sym = (sym).grey
return sym
}
/**
* Add an object table
*
* @param {Object} obj
* @param {Object} [data]
* @private
*/
_object(obj, data){
// handle data
data = Object.assign({
title: null,
colors: ['cyan', 'green', 'yellow', 'red', 'magenta'],
pathLabel: '#',
padKey: 15,
padVal: 45,
maxLevel: this.level,
stdWidth: Log.getTerminalWidth()
}, data)
// gather structural layout data
const compile = (objSub, level) => {
// handle array
if(Array.isArray(objSub)){
// count key length
data.padKey = Math.max(data.padKey, objSub.length)
// handle values (can go deeper)
return objSub.forEach((val, indexArray) => compile(val, level+1, indexArray))
}
// handle object
if(objSub && typeof objSub === 'object'){
// add obj.toString name (40/60 to key & val)
data.padKey = Math.max(data.padKey, Math.round(objSub.toString().length *.4)+2)
data.padVal = Math.max(data.padVal, Math.round(objSub.toString().length *.6)+2)
// handle first level of keys
return Object.keys(objSub).forEach((key, indexObj) => {
// count key length
data.padKey = Math.max(data.padKey, (key+'').length)
// handle value (can go deeper)
compile(objSub[key], level+1, indexObj)
})
}
// count level
data.maxLevel = Math.max(data.maxLevel, level)
// handle value
data.padVal = Math.max(data.padVal, (objSub+'').length)
}
/**
* Insert any object
*
* @param {Object} objSub
* @param {Log} parent
* @param {String} [title]
* @param {Array} [path]
* @type {function(this:Log)}
*/
function insert(objSub, parent, title, path){
if(!path) path = [data.pathLabel]
// create sub box
const keys = Object.keys(objSub)
const box = parent.box().over()
// calc colors
let color = data.colors[(box.level-this.level-1) % data.colors.length]
let objStr
if(objSub instanceof Function){
objStr = '[Function]'
color = 'blue'
}
else if(objSub instanceof Array){
objStr = '[object Array]'
}
else
objStr = '[object Object]'
// set color
box.options({color})
// title "──────[object Object]─┤"
if(!title){
title = objStr
title = Log.col(Log.pad('─', data.pad - box.level - (title.length)-3), 'dim')
+ title
+ Log.col('─┐', 'dim')
}
// add title
if(objStr !== '[Function]')
box.line(title, 'pre:')
// build function
if(objSub instanceof Function){
const functStr = objSub.toString().replace(/\t/g, ' ').replace(/\r/g, '').split('\n')
const spacer = (functStr.length+'').length
const functPad = Math.max(data.pad, Log.getTerminalWidth())
return functStr.forEach(function(line, i){
box.line(
// prefix
Log.col(i+1,'grey')
+ '─'
// code & spacer
+ Log.col(
Log.truncate(line, data.pad - box.level - 4 - spacer)
+ Log.pad(' ', functPad - line.length - box.level - 4 - spacer)
, 'code'
)
, 'pre:─'+Log.pad('─', spacer - ((i+1)+'').length)
)
})
}
// no properties found
else if(!keys.length){
let emptyStr = '{ empty object }'
let emptyPad = (data.pad - box.level - emptyStr.length)-3
let padLeft = Math.floor(emptyPad / 2)
let padRight = padLeft < emptyPad/2 ? padLeft+1 : padLeft
emptyStr = Log.col(
Log.pad('─', padLeft)
+ Log.col(emptyStr, 'bold')
+ Log.pad('─', padRight)
+ (box.level-this.level>1?'┤':'─')
+ Log.col(box.level-this.level>1?'│':'┤', data.colors[0])
, 'dim')
box.line(emptyStr, 'pre:')
}
// iterate object keys
keys.forEach((key, iKeys) => {
let val = objSub[key]
if(val instanceof Date){
val = Log.col(val.toISOString(), 'blue')
}
// sub object
else if(val && typeof val === 'object'){
// build path
path[box.level+1] = (parseInt(key) == String(key) ? '['+key+']' : '.'+key)
// title "key────[object Object]─┤"
let title = Array.isArray(val) ? '[object Array]' : val.toString()
title = Log.col(key)
+ Log.col(
Log.pad('─',
data.pad - (box.level) - colors.stripColor(key).length - colors.stripColor(title).length -5)
, 'dim')
+ title
+ Log.col('─┬', 'dim')
+ Log.col('┤', data.colors[0], 'dim')
return insert.call(this, val, box, title, path)
}
// truncate key
key = Log.truncate(key, data.padKey-box.level-2)
// prepare
let valueLines = [val]
const line = {
prefix: [
key
+ Log.col(Log.pad('·', data.padKey-key.length-box.level)+': ', 'dim')
, Log.pad(' ', data.padKey-box.level+2)
]
}
// function
if(val instanceof Function){
valueLines = val.toString()
.replace(/\t/g, ' ')
.replace(/\r/g, '')
.split('\n')
}
// break value into multiple lines
else if(typeof val === 'string'){
valueLines = Log.wrap(val, data.padVal-9).replace(/\r/g, '').split('\n')
}
// insert lines
line.lines = valueLines.length-1
valueLines.forEach((val, i) => {
// convert val to string
val = Log.parseWord(val)
// build "title···: value │"
box.line(
// prefix
line.prefix[ i < 1 ? 0 : 1 ]
// value
+ Log.col(Log.truncate(val, data.padVal-6), 'grey')
// spacer
+ Log.pad(' ', data.padVal-colors.stripColor(val).length-6)
// postfix
+ Log.col(
// is min 2nd lvl
(box.level - this.level > 1
// is last of any > lvl1
?
(i === valueLines.length-1 && keys[iKeys+1] && objSub[keys[iKeys+1]] !== null && typeof objSub[keys[iKeys+1]] === 'object'
? '↓'
: (i === 0 && keys[iKeys-1] && objSub[keys[iKeys-1]] !== null && typeof objSub[keys[iKeys-1]] === 'object'
? '↑'
: '│')
)
: ' '
), color, 'dim'
)
+ Log.col('│', data.colors[0], 'dim')
, 'pre:'+(line.lines && line.lines === i?'┘':(line.lines && i<1?'┐':(line.lines?'┤':'─')))
)
})
})
// insert footer
const footerStr = Log.truncate(path.slice(0, box.level+1).join(''), data.pad - box.level - 11)
+ '─('
+ (keys.length+'')
+ ')─'
box.line(Log.col(
Log.pad('─', (data.pad - (box.level) - footerStr.length - 3))
+ footerStr
+ (box.parent && box.parent.level === this.level
? Log.col('─┘', 'dim')
: Log.col('┴', 'dim') + Log.col('┤', data.colors[0], 'dim')
), 'dim'), 'pre:')
}
// run calculations for layout structure
compile(obj, this.level)
// add ": " and level to value
data.padKey += data.maxLevel
data.padVal += 2
// calc needed with
data.pad = data.padKey + data.padVal
// set max with
if(data.pad > data.stdWidth){
data.pad = data.stdWidth
if(data.padKey > data.pad){
data.padKey = Math.round(data.pad * .7)
data.padVal = Math.round(data.pad * .3)
} else {
data.padVal = data.pad - data.padKey
}
}
// insert
insert.call(this, obj, this)
}
}
// /**
// * Truncate string containing ansi color codes
// * (not in use)
// *
// * Source:
// * http://stackoverflow.com/questions/26238553
// *
// * @param {String} str
// * @param {Number} len
// * @param {String} [postfix]
// * @returns {String}
// */
//Log.truncateAnsi = function(str, len, postfix){
// // default postfix
// if(!postfix) postfix = '…'
//
// // substract postfix from length
// len -= postfix.length
//
// if (!len || len < 10) return str; // probably not a valid console -- send back the whole line
// let count = 0, // number of visible chars on line so far
// esc = false, // in an escape sequence
// longesc = false // in a multi-character escape sequence
// let outp = true // should output this character
// let arr = str.split('')
// let res = arr.filter(function(c){ // filter characters...
// if (esc && !longesc && c == '[') longesc = true // have seen an escape, now '[', start multi-char escape
// if (c == '\x1b') esc = true // start of escape sequence
//
// outp = (count < len || esc) // if length exceeded, don't output non-escape chars
// if (!esc && !longesc) count++ // if not in escape, count visible char
//
// if (esc && !longesc && c != '\x1b') esc = false // out of single char escape
// if (longesc && c != '[' && c >= '@' && c <= '~') {esc=false; longesc=false;} // end of multi-char escape
//
// return outp // result for filter
// })
//
// if(arr.length == res.length)
// return res.join('')
//
// return res.slice(0, res.length-7).join('') + "\x1b[0m" + postfix
//}
// instance
const log = new Log
module.exports = function(opt){
// config
log.options(opt)
if(log.opt.isWorker){
log.opt.disableWelcome = true
log.printedLines = 1
}
else if(!log.opt.disableWelcome)
log.log(log.col(pkg.name, 'rainbow')+' v'+pkg.version+' - Hi mate')
if(log.opt.override)
log.overrideConsole()
return log
}