Dev: Console refactoring

This commit is contained in:
surunzi
2016-08-22 22:06:55 +08:00
parent 9ff8d8cd22
commit 7d47d007a4
9 changed files with 471 additions and 352 deletions

19
doc/Tool_Api.md Normal file
View File

@@ -0,0 +1,19 @@
# Tool Api
## Console
## Elements
## Network
## Resources
## Sources
## Info
## Snippets
## Features
## Settings

View File

@@ -1,4 +1,4 @@
import Log from './Logger.es6'
import Logger from './Logger.es6'
import Tool from '../DevTools/Tool.es6'
import util from '../lib/util'
import config from '../lib/config.es6'
@@ -17,7 +17,7 @@ export default class Console extends Tool
this._appendTpl();
this._initLogger();
this._exposeLog();
this._exposeLogger();
this._initConfig(parent);
this._bindEvent(parent);
}
@@ -104,10 +104,10 @@ export default class Console extends Tool
$this[isMatch ? 'addClass' : 'rmClass']('eruda-active');
}));
}
_exposeLog()
_exposeLogger()
{
let logger = this._logger,
methods = ['filter'].concat(CONSOLE_METHOD);
methods = ['filter', 'html'].concat(CONSOLE_METHOD);
methods.forEach(name => this[name] = (...args) =>
{
@@ -196,7 +196,7 @@ export default class Console extends Tool
if (cfg.get('catchGlobalErr')) this.catchGlobalErr();
if (cfg.get('overrideConsole')) this.overrideConsole();
if (cfg.get('displayExtraInfo')) logger.displayExtraInfo(true);
logger.setMaxNum(maxLogNum);
logger.maxNum(maxLogNum);
cfg.on('change', (key, val) =>
{
@@ -204,7 +204,7 @@ export default class Console extends Tool
{
case 'catchGlobalErr': return val ? this.catchGlobalErr() : this.ignoreGlobalErr();
case 'overrideConsole': return val ? this.overrideConsole() : this.restoreConsole();
case 'maxLogNum': return logger.setMaxNum(val === 'infinite' ? val : +val);
case 'maxLogNum': return logger.maxNum(val === 'infinite' ? val : +val);
case 'displayExtraInfo': return logger.displayExtraInfo(val);
}
});

View File

@@ -7,7 +7,7 @@
<span class="eruda-filter filter" data-filter="log">Log</span>
<span class="eruda-icon-info-circle help" ontouchstart></span>
</div>
<div class="eruda-logs"></div>
<ul class="eruda-logs"></ul>
<div class="eruda-js-input">
<div class="eruda-buttons">
<div class="eruda-button cancel" ontouchstart>Cancel</div>

View File

@@ -1,11 +1,242 @@
import util from '../lib/util'
import stringify from '../lib/stringify.es6'
import highlight from '../lib/highlight.es6'
import beautify from 'js-beautify'
export default class Log
{
constructor(type, args)
constructor({type, args, idx, header})
{
this._type = type;
this._args = args;
this._timestamp = util.now();
this._idx = idx;
this._header = header;
this._ignoreFilter = false;
this._preProcess();
}
get formattedMsg()
{
if (!this._formattedMsg) this._formatMsg();
return this._formattedMsg;
}
get ignoreFilter()
{
return this._ignoreFilter;
}
get type()
{
return this._type;
}
_preProcess()
{
switch (this._type)
{
case 'input':
case 'output':
this._ignoreFilter = true;
break;
}
if (this._header)
{
this._time = getCurTime();
this._from = getFrom();
}
}
_formatMsg()
{
let type = this._type,
idx = this._idx,
hasHeader = this._header,
time = this._time,
from = this._from,
args = this._args;
let msg = '', icon;
switch (type)
{
case 'log':
case 'info':
case 'warn':
msg = formatMsg(args);
break;
case 'error':
let err = args[0];
icon = 'times-circle';
err = util.isErr(args[0]) ? args[0] : new Error(err);
msg = formatErr(err);
break;
case 'table':
msg = formatTable(args);
break;
case 'html':
msg = args[0];
break;
case 'input':
msg = formatJs(args[0]);
icon = 'chevron-right';
break;
case 'output':
msg = formatMsg(args);
icon = 'chevron-left';
break;
}
msg = render({msg, type, icon, idx, hasHeader, time, from});
this.src = stringify(this._args);
delete this._args;
this._formattedMsg = msg;
}
}
function formatTable(args)
{
return '';
}
var regJsUrl = /https?:\/\/([0-9.\-A-Za-z]+)(?::(\d+))?\/[A-Z.a-z0-9/]*\.js/g;
function formatErr(err)
{
var lines = err.stack.split('\n'),
msg = `${err.message || lines[0]}<br/>`;
lines = lines.filter(val => val.indexOf('eruda') < 0);
var stack = `<div class="eruda-stack">${lines.slice(1).join('<br/>')}</div>`;
return msg + stack.replace(regJsUrl, match => `<a href="${match}" target="_blank">${match}</a>`);
}
function formatJs(code)
{
return highlight(beautify(code), 'js');
}
function formatMsg(args)
{
if (util.isStr(args[0])) args = substituteStr(args);
for (let i = 0, len = args.length; i < len; i++)
{
let val = args[i];
if (util.isEl(val))
{
args[i] = formatEl(val);
} else if (util.isFn(val))
{
args[i] = formatFn(val);
} else if (util.isObj(val))
{
args[i] = formatObj(val);
}else if (util.isUndef(val))
{
args[i] = 'undefined';
} else if (util.isNull(val))
{
args[i] = 'null';
} else
{
args[i] = util.escape(util.toStr(val));
}
}
return args.join(' ');
}
function substituteStr(args)
{
var str = args[0],
newStr = '';
args.shift();
for (let i = 0, len = str.length; i < len; i++)
{
let c = str[i];
if (c === '%')
{
i++;
let arg = args.shift();
switch (str[i])
{
case 'd':
newStr += util.toNum(arg);
break;
case 's':
newStr += util.toStr(arg);
break;
case 'o':
try {
newStr += JSON.stringify(arg);
} catch (e) {}
break;
default:
i--;
args.unshift(arg);
newStr += c;
}
} else
{
newStr += c;
}
}
args.unshift(newStr);
return args;
}
function formatObj(val)
{
return `${util.upperFirst(typeof val)} ${JSON.stringify(extractObj(val, true))}`;
}
function formatFn(val)
{
return val.toString();
}
function formatEl(val)
{
return `<pre>${highlight(beautify.html(val.outerHTML), 'html')}</pre>`;
}
function getCurTime()
{
let d = new Date();
return `${padZero(d.getHours())}:${padZero(d.getMinutes())}:${padZero(d.getSeconds())}`;
}
function getFrom()
{
let e = new Error(),
ret = '',
lines = e.stack.split('\n');
for (let i = 0, len = lines.length; i < len; i++)
{
ret = lines[i];
if (ret.indexOf('winConsole') > -1 && i < len - 1)
{
ret = lines[i+1];
break;
}
}
return ret;
}
var padZero = (num) => util.lpad(util.toStr(num), 2, '0');
var tpl = require('./Log.hbs');
var render = data => tpl(data);
var extractObj = (obj, simple) => JSON.parse(stringify(obj, null, obj, simple));

20
src/Console/Log.hbs Normal file
View File

@@ -0,0 +1,20 @@
<li>
{{#if hasHeader}}
<div class="eruda-header">
{{time}} {{from}}
</div>
{{/if}}
<div class="eruda-{{type}} eruda-log-item" data-idx="{{idx}}">
{{#if icon}}
<div class="eruda-icon-container">
<span class="eruda-icon eruda-icon-{{icon}}"></span>
</div>
{{/if}}
{{#if showTimes}}<div class="eruda-times">{{times}}</div>{{/if}}
<div class="eruda-log-content-wrapper">
<div class="eruda-log-content">
{{{msg}}}
</div>
</div>
</div>
</li>

View File

@@ -2,179 +2,64 @@ import util from '../lib/util'
import stringify from '../lib/stringify.es6'
import highlight from '../lib/highlight.es6'
import beautify from 'js-beautify'
import Log from './Log'
import Log from './Log.es6'
export default class Logger extends util.Emitter
{
constructor($el, parent)
{
super();
util.evalCss(require('./Logger.scss'));
this._$el = $el;
this._parent = parent;
this._logs = [];
this._renderLogs = [];
this._tpl = require('./Logger.hbs');
this._timer = {};
this._filter = 'all';
this._maxNum = 'infinite';
this._displayExtraInfo = false;
this._isUpdated = false;
this._lastLog = {};
this._timer = {};
this._bindEvent();
}
setMaxNum(num)
{
var logs = this._logs;
this._maxNum = num;
if (util.isNum(num) && logs.length > num)
{
this._logs = logs.slice(logs.length - num);
this._isUpdated = true;
this.render();
}
}
displayExtraInfo(flag)
{
this._displayExtraInfo = flag;
}
clear()
maxNum(val)
{
this._logs = [];
this._lastLog = {};
this._isUpdated = true;
var logs = this._logs;
this._maxNum = val;
if (util.isNum(val) && logs.length > val)
{
this._logs = logs.slice(logs.length - val);
this._isUpdated = true;
this.render();
}
}
filter(val)
{
this._filter = val;
this.emit('filter', val);
return this.render();
}
input(jsCode)
{
if (util.startWith(jsCode, ':'))
{
this._runCmd(jsCode.slice(1));
return this;
} else if (util.startWith(jsCode, '/'))
{
return this.filter(new RegExp(util.escapeRegExp(jsCode.slice(1))));
}
this.insert({
type: 'input',
ignoreFilter: true,
isCode: true,
icon: 'chevron-right',
src: jsCode,
val: jsCode
});
try {
this.output(evalJs(jsCode));
} catch (e)
{
e.ignoreFilter = true;
this.error(e);
}
return this;
}
output(val)
{
return this.insert({
type: 'output',
ignoreFilter: true,
icon: 'chevron-left',
src: util.isObj(val) ? extractObj(val) : val,
val: transMsg(val)
});
}
dir(obj)
{
var src = util.isObj(obj) ? extractObj(obj) : obj,
msg;
if (util.isObj(src))
{
msg = JSON.stringify(src, null, 4);
msg = msg.replace(/erudaProto/g, '__proto__')
.replace(/erudaObjAbstract/g, '__abstract__');
} else
{
msg = transMsg(src, true);
}
return this.insert({
type: 'dir',
isCode: true,
src,
val: msg
});
}
log(...args)
{
return this.insert({
type: 'log',
src: extractSrc(args),
val: transMultipleMsg(args)
});
}
html(val)
{
return this.insert({
type: 'html',
ignoreFilter: true,
val
});
}
error(msg)
{
if (util.isUndef(msg)) return;
this.insert('log', args);
if (!util.isErr(msg)) msg = new Error(msg);
return this;
}
dir(...args)
{
this.insert('dir', args);
return this.insert({
type: 'error',
ignoreFilter: msg.ignoreFilter,
src: {
message: msg.message || '',
stack: msg.stack
},
icon: 'times-circle',
val: errToStr(msg)
});
return this;
}
info(...args)
table(...args)
{
return this.insert({
type: 'info',
src: extractSrc(args),
icon: 'info-circle',
val: transMultipleMsg(args)
});
}
warn(...args)
{
return this.insert({
type: 'warn',
src: extractSrc(args),
icon: 'exclamation-triangle',
val: transMultipleMsg(args)
});
}
filter(type)
{
this._filter = type;
this.emit('filter', type);
this._isUpdated = true;
return this.render();
}
help()
{
return this.html(helpMsg);
this.insert('table', args);
return this;
}
time(name)
{
@@ -191,107 +76,95 @@ export default class Logger extends util.Emitter
return this.html(`<div class="eruda-blue">${name}: ${util.now() - startTime}ms</div>`);
}
insert(log)
clear()
{
var logs = this._logs;
this._logs = [];
if (this._maxNum !== 'infinite' && logs.length >= this._maxNum) logs.shift();
util.defaults(log, {
type: 'log',
isCode: false,
ignoreFilter: false,
val: '',
showTimes: false,
times: 1
});
log.src = log.src || log.val;
if (log.isCode)
{
log.val = highlight(beautify(log.val), 'js');
} else if (log.type != 'html')
{
log.val = txtToHtml(log.val);
}
if (this._displayExtraInfo)
{
log.hasHeader = true;
log.time = getCurTime();
log.from = getFrom();
}
var lastLog = this._lastLog;
if (log.type !== 'html' &&
lastLog.type === log.type &&
lastLog.val === log.val)
{
lastLog.times++;
lastLog.showTimes = true;
if (log.time) lastLog.time = log.time;
} else
{
logs.push(log);
this._lastLog = log;
}
this.emit('insert', log);
this._isUpdated = true;
return this.render();
}
_bindEvent()
info(...args)
{
var self = this;
this._$el.on('click', '.eruda-log-item', function ()
{
var idx = util.$(this).data('idx'),
src = self._renderLogs[idx].src;
try {
if (!util.isObj(src)) src = JSON.parse(src);
self.emit('viewJson', src);
} catch (e) {}
});
return this.insert('info', args);
}
_runCmd(cmd)
error(...args)
{
switch (cmd.trim())
return this.insert('error', args);
}
warn(...args)
{
return this.insert('warn', args);
}
input(jsCode)
{
if (util.startWith(jsCode, ':'))
{
case '$': return this._loadJs('jQuery');
case '_': return this._loadJs('underscore');
default:
this.warn('Unknown command').help();
this._runCmd(jsCode.slice(1));
return this;
} else if (util.startWith(jsCode, '/'))
{
return this.filter(new RegExp(util.escapeRegExp(jsCode.slice(1))));
}
}
_loadJs(name)
{
util.loadJs(libraries[name], (result) =>
{
if (result) return this.log(`${name} is loaded`);
this.warn(`Failed to load ${name}`);
});
this.insert('input', [jsCode]);
try {
this.output(evalJs(jsCode));
} catch (e)
{
this.error(e);
}
return this;
}
output(val)
{
return this.insert('output', [val]);
}
html(...args)
{
return this.insert('html', args);
}
help()
{
return this.html(helpMsg);
}
render()
{
if (!this._parent.active || !this._isUpdated) return;
this._isUpdated = false;
let html = '',
logs = this._logs;
var logs = this._renderLogs = this._filterLogs(this._logs);
logs = this._renderLogs = this._filterLogs(logs);
for (let i = 0, len = logs.length; i < len; i++)
{
html += logs[i].formattedMsg;
}
this._renderHtml(this._tpl({logs: logs}));
this._scrollToBottom();
}
_renderHtml(html)
{
if (html === this._lastHtml) return;
this._lastHtml = html;
this._$el.html(html);
this.scrollToBottom();
return this;
}
insert(type, args)
{
let logs = this._logs;
let log = new Log({
type, args,
idx: logs.length,
header: this._displayExtraInfo
});
logs.push(log);
this.render();
return this;
}
scrollToBottom()
{
var el = this._$el.get(0);
el.scrollTop = el.scrollHeight;
}
_filterLogs(logs)
{
@@ -310,11 +183,39 @@ export default class Logger extends util.Emitter
return val.ignoreFilter || val.type === filter;
});
}
_scrollToBottom()
_loadJs(name)
{
var el = this._$el.get(0);
util.loadJs(libraries[name], (result) =>
{
if (result) return this.log(`${name} is loaded`);
el.scrollTop = el.scrollHeight;
this.warn(`Failed to load ${name}`);
});
}
_runCmd(cmd)
{
switch (cmd.trim())
{
case '$': return this._loadJs('jQuery');
case '_': return this._loadJs('underscore');
default:
this.warn('Unknown command').help();
}
}
_bindEvent()
{
var self = this;
this._$el.on('click', '.eruda-log-item', function ()
{
var idx = util.$(this).data('idx'),
src = self._renderLogs[idx].src;
try {
if (!util.isObj(src)) src = JSON.parse(src);
self.emit('viewJson', src);
} catch (e) {}
});
}
}
@@ -322,8 +223,6 @@ var cmdList = require('./cmdList.json'),
helpMsg = require('./help.hbs')({commands: cmdList}),
libraries = require('./libraries.json');
var regJsUrl = /https?:\/\/([0-9.\-A-Za-z]+)(?::(\d+))?\/[A-Z.a-z0-9/]*\.js/g;
var evalJs = jsInput =>
{
var ret;
@@ -336,84 +235,3 @@ var evalJs = jsInput =>
return ret;
};
function errToStr(err)
{
var lines = err.stack.split('\n'),
msg = `${err.message || lines[0]}<br/>`;
lines = lines.filter(val => val.indexOf('eruda') < 0);
var stack = `<div class="eruda-stack">${lines.slice(1).join('<br/>')}</div>`;
return msg + stack.replace(regJsUrl, match => `<a href="${match}" target="_blank">${match}</a>`);
}
function transMsg(msg, noEscape)
{
if (util.isEl(msg))
{
msg = `<pre>${highlight(beautify.html(msg.outerHTML), 'html')}</pre>`;
noEscape = true;
} else if (util.isFn(msg))
{
msg = msg.toString();
} else if (util.isObj(msg))
{
msg = `${util.upperFirst(typeof msg)} ${JSON.stringify(extractObj(msg, true))}`;
} else if (util.isUndef(msg))
{
msg = 'undefined';
} else if (msg === null)
{
msg = 'null';
}
msg = util.toStr(msg);
if (noEscape) return msg;
return util.escape(msg);
}
function extractSrc(args)
{
if (args.length !== 1) return args;
return util.isObj(args[0]) ? extractObj(args[0]) : args[0];
}
function getCurTime()
{
let d = new Date();
return `${padZero(d.getHours())}:${padZero(d.getMinutes())}:${padZero(d.getSeconds())}`;
}
function getFrom()
{
let e = new Error(),
ret = '',
lines = e.stack.split('\n');
for (let i = 0, len = lines.length; i < len; i++)
{
ret = lines[i];
if (ret.indexOf('winConsole') > -1 && i < len - 1)
{
ret = lines[i+1];
break;
}
}
return ret;
}
var padZero = (num) => util.lpad(util.toStr(num), 2, '0');
var extractObj = (obj, simple) => JSON.parse(stringify(obj, null, obj, simple));
var transMultipleMsg = args => args.map(val => transMsg(val)).join(' ');
var txtToHtml = str => str.replace(/\n/g, '<br/>')
.replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;');

View File

@@ -1,28 +0,0 @@
<ul>
{{#each logs}}
<li>
{{#if hasHeader}}
<div class="eruda-header">
{{time}} {{from}}
</div>
{{/if}}
<div class="eruda-{{type}} eruda-log-item" data-idx="{{@index}}">
{{#if icon}}
<div class="eruda-icon-container">
<span class="eruda-icon eruda-icon-{{icon}}"></span>
</div>
{{/if}}
{{#if showTimes}}<div class="eruda-times">{{times}}</div>{{/if}}
<div class="eruda-log-content-wrapper">
<div class="eruda-log-content">
{{#if isCode}}
<pre>{{{val}}}</pre>
{{else}}
{{{val}}}
{{/if}}
</div>
</div>
</div>
</li>
{{/each}}
</ul>

View File

@@ -1296,6 +1296,30 @@ module.exports = (function ()
return exports;
})();
/* ------------------------------ isNull ------------------------------ */
var isNull = _.isNull = (function ()
{
/* Check if value is an Null.
*
* |Name |Type |Desc |
* |------|-------|-----------------------|
* |value |* |Value to check |
* |return|boolean|True if value is an Null|
*
* ```javascript
* isNull(null); // -> true
* ```
*/
function exports(val)
{
return val === null;
}
return exports;
})();
/* ------------------------------ isRegExp ------------------------------ */
var isRegExp = _.isRegExp = (function ()

View File

@@ -18,12 +18,47 @@ describe('log', function ()
tool.clear().log(obj);
expect($tool.find('.eruda-log')).toContainText('Object {"a":1}');
});
it('html', function ()
{
tool.clear().html('<span class="color-blue">Blue</span>');
expect($tool.find('.eruda-html')).toContainElement('span.color-blue');
});
it('timer', function ()
{
tool.clear().time('eruda');
tool.clear().timeEnd('eruda');
expect($tool.find('.eruda-html')).toHaveText(/eruda: \d+ms/);
});
});
describe('substitution', function ()
{
it('number', function ()
{
tool.clear().log('Eruda is %d', 1, 'year old');
expect($tool.find('.eruda-log')).toContainText('Eruda is 1 year old');
});
it('string', function ()
{
tool.clear().log('My name is %s', 'eruda');
expect($tool.find('.eruda-log')).toContainText('My name is eruda');
});
it('object', function ()
{
tool.clear().log('Object is %o', {a: 1});
expect($tool.find('.eruda-log')).toContainText('Object is {"a":1}');
});
});
describe('filter', function ()
{
// Test case from https://github.com/liriliri/eruda/issues/14
it('function', function ()
/*it('function', function ()
{
tool.clear().filter(function (log)
{
@@ -41,5 +76,5 @@ describe('filter', function ()
});
tool.log(obj);
expect($tool.find('.eruda-logs li').length).toEqual(1);
});
});*/
});