eruda2
Version:
Console for Mobile Browsers
575 lines (508 loc) • 13.3 kB
JavaScript
import origGetAbstract from '../lib/getAbstract'
import beautify from 'js-beautify'
import LunaObjectViewer from 'luna-object-viewer'
import {
isObj,
isStr,
isErr,
isPrimitive,
wrap,
defaults,
getObjType,
isEl,
toStr,
toNum,
toInt,
escape,
isNull,
isUndef,
isFn,
toArr,
isArr,
unique,
contain,
isEmpty,
clone,
noop,
highlight,
each,
trim,
lowerCase,
keys,
$,
Emitter,
stringifyAll,
nextTick,
linkify,
} from '../lib/util'
import evalCss from '../lib/evalCss'
export default class Log extends Emitter {
static showGetterVal = false
static showUnenumerable = true
static lazyEvaluation = true
constructor({
type = 'log',
args = [],
id,
group = {},
targetGroup = {},
headers,
ignoreFilter = false,
}) {
super()
this.type = type
this.group = group
this.targetGroup = targetGroup
this.args = args
this.count = 1
this.id = id
this.headers = headers
this.ignoreFilter = ignoreFilter
this.collapsed = false
this.el = document.createElement('div')
this.el.log = this
this.height = 0
this.width = 0
this._$el = $(this.el)
this._formatMsg()
if (this.group) {
this.checkGroup()
}
}
// If state changed, return true.
checkGroup() {
let { group } = this
let collapsed = false
while (group) {
if (group.collapsed) {
collapsed = true
break
}
group = group.parent
}
if (collapsed !== this.collapsed) {
this.collapsed = collapsed
return true
}
return false
}
updateIcon(icon) {
const $icon = this._$el.find('.eruda-icon')
$icon.rmAttr('class').addClass(['eruda-icon', `eruda-icon-${icon}`])
return this
}
addCount() {
this.count++
const count = this.count
const $el = this._$el
const $container = $el.find('.eruda-count-container')
const $icon = $el.find('.eruda-icon-container')
const $count = $container.find('.eruda-count')
if (count === 2) {
$container.rmClass('eruda-hidden')
}
$count.text(count)
$icon.addClass('eruda-hidden')
return this
}
groupEnd() {
const $el = this._$el
const $lastNesting = $el
.find('.eruda-nesting-level:not(.eruda-group-closed)')
.last()
$lastNesting.addClass('eruda-group-closed')
return this
}
updateTime(time) {
const $el = this._$el
const $container = $el.find('.eruda-time-container')
if (this.time) {
$container.find('span').eq(0).text(time)
this.time = time
}
return this
}
isAttached() {
return !!this.el.parentNode
}
updateSize(silent = true) {
const height = this.el.offsetHeight
const width = this.el.offsetWidth
if (this.height !== height || this.width !== width) {
this.height = height
this.width = width
if (!silent) this.emit('updateSize')
}
}
html() {
return this.el.outerHTML
}
text() {
return this._content.textContent
}
_needSrc() {
const { type, args } = this
if (type === 'html') return false
for (let i = 0, len = args.length; i < len; i++) {
if (isObj(args[i])) return true
}
return false
}
extractObj(cb = noop) {
const { args, type } = this
const setSrc = (result) => {
this.src = result
cb()
}
if (type === 'table') {
extractObj(args[0], {}, setSrc)
} else {
extractObj(
args.length === 1 && isObj(args[0]) ? args[0] : args,
{},
setSrc
)
}
}
click(logger) {
const { type, src } = this
let { args } = this
const $el = this._$el
switch (type) {
case 'log':
case 'warn':
case 'info':
case 'debug':
case 'output':
case 'table':
case 'dir':
case 'group':
case 'groupCollapsed':
if (src || args) {
const $json = $el.find('.eruda-json')
if ($json.hasClass('eruda-hidden')) {
if ($json.data('init') !== 'true') {
if (src) {
const staticViewer = new LunaObjectViewer.Static($json.get(0))
staticViewer.set(src)
staticViewer.on('change', () => this.updateSize(false))
} else {
if (type === 'table' || args.length === 1) {
if (isObj(args[0])) args = args[0]
}
const objViewer = new LunaObjectViewer($json.get(0), {
unenumerable: Log.showUnenumerable,
accessGetter: Log.showGetterVal,
})
objViewer.set(args)
objViewer.on('change', () => this.updateSize(false))
}
$json.data('init', 'true')
}
$json.rmClass('eruda-hidden')
} else {
$json.addClass('eruda-hidden')
}
} else if (type === 'group' || type === 'groupCollapsed') {
logger.toggleGroup(this)
}
break
case 'error':
$el.find('.eruda-stack').toggleClass('eruda-hidden')
break
}
this.updateSize(false)
}
_formatMsg() {
let { args } = this
const { type, id, headers, group } = this
// Don't change original args for lazy evaluation.
args = clone(args)
if (this._needSrc() && !Log.lazyEvaluation) {
this.extractObj()
}
let msg = ''
let icon
let err
if (type === 'group' || type === 'groupCollapsed') {
if (args.length === 0) {
args = ['console.group']
}
}
switch (type) {
case 'log':
msg = formatMsg(args)
break
case 'debug':
msg = formatMsg(args)
break
case 'dir':
msg = formatDir(args)
break
case 'info':
msg = formatMsg(args)
break
case 'warn':
icon = 'warn'
msg = formatMsg(args)
break
case 'error':
if (isStr(args[0]) && args.length !== 1) args = substituteStr(args)
err = args[0]
icon = 'error'
err = isErr(err) ? err : new Error(formatMsg(args))
this.src = err
msg = formatErr(err)
break
case 'table':
msg = formatTable(args)
break
case 'html':
msg = args[0]
break
case 'input':
msg = formatJs(args[0])
icon = 'arrow-right'
break
case 'output':
msg = formatMsg(args)
icon = 'arrow-left'
break
case 'groupCollapsed':
msg = formatMsg(args)
icon = 'caret-right'
break
case 'group':
msg = formatMsg(args)
icon = 'caret-down'
break
}
if (!this._needSrc() || !Log.lazyEvaluation) {
delete this.args
}
// Only linkify for simple types
if (type !== 'error' && !this.args) {
msg = linkify(msg, (url) => {
return `<a href="${url}" target="_blank">${url}</a>`
})
}
msg = render({ msg, type, icon, id, headers, group })
this._$el.addClass('eruda-log-container').html(msg)
this._$content = this._$el.find('.eruda-log-content')
this._content = this._$content.get(0)
}
}
const getAbstract = wrap(origGetAbstract, function (fn, obj) {
return (
'<span class="eruda-abstract">' +
fn(obj, {
getterVal: Log.showGetterVal,
unenumerable: false,
}) +
'</span>'
)
})
const Value = '__ErudaValue'
function formatTable(args) {
const table = args[0]
let ret = ''
let filter = args[1]
let columns = []
if (isStr(filter)) filter = toArr(filter)
if (!isArr(filter)) filter = null
if (!isObj(table)) return formatMsg(args)
each(table, (val) => {
if (isPrimitive(val)) {
columns.push(Value)
} else if (isObj(val)) {
columns = columns.concat(keys(val))
}
})
columns = unique(columns)
columns.sort()
if (filter) columns = columns.filter((val) => contain(filter, val))
if (columns.length > 20) columns = columns.slice(0, 20)
if (isEmpty(columns)) return formatMsg(args)
ret += '<table><thead><tr><th>(index)</th>'
columns.forEach(
(val) => (ret += `<th>${val === Value ? 'Value' : toStr(val)}</th>`)
)
ret += '</tr></thead><tbody>'
each(table, (obj, idx) => {
ret += `<tr><td>${idx}</td>`
columns.forEach((column) => {
if (isObj(obj)) {
ret +=
column === Value
? '<td></td>'
: `<td>${formatTableVal(obj[column])}</td>`
} else if (isPrimitive(obj)) {
ret +=
column === Value ? `<td>${formatTableVal(obj)}</td>` : '<td></td>'
}
})
ret += '</tr>'
})
ret += '</tbody></table>'
ret += '<div class="eruda-json eruda-hidden"></div>'
return ret
}
function formatTableVal(val) {
if (isObj(val)) return (val = '{…}')
if (isPrimitive(val)) return getAbstract(val)
return toStr(val)
}
const regJsUrl = /https?:\/\/([0-9.\-A-Za-z]+)(?::(\d+))?\/[A-Z.a-z0-9/]*\.js/g
const regErudaJs = /eruda(\.min)?\.js/
function formatErr(err) {
let lines = err.stack ? err.stack.split('\n') : []
const msg = `${err.message || lines[0]}<br/>`
lines = lines.filter((val) => !regErudaJs.test(val)).map((val) => escape(val))
const stack = `<div class="eruda-stack eruda-hidden">${lines
.slice(1)
.join('<br/>')}</div>`
return (
msg +
stack.replace(
regJsUrl,
(match) => `<a href="${match}" target="_blank">${match}</a>`
)
)
}
function formatJs(code) {
const curTheme = evalCss.getCurTheme()
return highlight(beautify(code, { indent_size: 2 }), 'js', {
keyword: `color:${curTheme.keywordColor}`,
number: `color:${curTheme.numberColor}`,
operator: `color:${curTheme.operatorColor}`,
comment: `color:${curTheme.commentColor}`,
string: `color:${curTheme.stringColor}`,
})
}
function formatMsg(args, { htmlForEl = true } = {}) {
const needStrSubstitution = isStr(args[0]) && args.length !== 1
if (needStrSubstitution) args = substituteStr(args)
for (let i = 0, len = args.length; i < len; i++) {
let val = args[i]
if (isEl(val) && htmlForEl) {
args[i] = formatEl(val)
} else if (isFn(val)) {
args[i] = formatFn(val)
} else if (isObj(val)) {
args[i] = formatObj(val)
} else if (isUndef(val)) {
args[i] = 'undefined'
} else if (isNull(val)) {
args[i] = 'null'
} else {
val = toStr(val)
if (i !== 0 || !needStrSubstitution) val = escape(val)
args[i] = val
}
}
return args.join(' ') + '<div class="eruda-json eruda-hidden"></div>'
}
const formatDir = (args) => formatMsg(args, { htmlForEl: false })
function substituteStr(args) {
const str = escape(args[0])
let isInCss = false
let newStr = ''
args.shift()
for (let i = 0, len = str.length; i < len; i++) {
const c = str[i]
if (c === '%' && args.length !== 0) {
i++
const arg = args.shift()
switch (str[i]) {
case 'i':
case 'd':
newStr += toInt(arg)
break
case 'f':
newStr += toNum(arg)
break
case 's':
newStr += toStr(arg)
break
case 'O':
if (isObj(arg)) {
newStr += getAbstract(arg)
}
break
case 'o':
if (isEl(arg)) {
newStr += formatEl(arg)
} else if (isObj(arg)) {
newStr += getAbstract(arg)
}
break
case 'c':
if (str.length <= i + 1) {
break
}
if (isInCss) newStr += '</span>'
isInCss = true
newStr += `<span style="${correctStyle(arg)}">`
break
default:
i--
args.unshift(arg)
newStr += c
}
} else {
newStr += c
}
}
if (isInCss) newStr += '</span>'
args.unshift(newStr)
return args
}
function correctStyle(val) {
val = lowerCase(val)
const rules = val.split(';')
const style = {}
each(rules, (rule) => {
if (!contain(rule, ':')) return
const [name, val] = rule.split(':')
style[trim(name)] = trim(val)
})
style['display'] = 'inline-block'
style['max-width'] = '100%'
style['contain'] = 'paint'
delete style.width
delete style.height
let ret = ''
each(style, (val, key) => {
ret += `${key}:${val};`
})
return ret
}
function formatObj(val) {
let type = getObjType(val)
if (type === 'Array' && val.length > 1) type = `(${val.length})`
return `${type} ${getAbstract(val)}`
}
function formatFn(val) {
return `<pre style="display:inline">${formatJs(val.toString())}</pre>`
}
function formatEl(val) {
return `<pre style="display:inline">${highlight(
beautify.html(val.outerHTML, { unformatted: [], indent_size: 2 }),
'html'
)}</pre>`
}
const tpl = require('./Log.hbs')
const render = (data) => tpl(data)
function extractObj(obj, options = {}, cb) {
defaults(options, {
accessGetter: Log.showGetterVal,
unenumerable: Log.showUnenumerable,
symbol: Log.showUnenumerable,
timeout: 1000,
})
stringify(obj, options, (result) => cb(JSON.parse(result)))
}
function stringify(obj, options, cb) {
const result = stringifyAll(obj, options)
nextTick(() => cb(result))
}