mirror of
https://github.com/liriliri/eruda.git
synced 2026-03-24 09:48:37 +08:00
315 lines
7.3 KiB
JavaScript
315 lines
7.3 KiB
JavaScript
import {
|
|
escapeJsonStr,
|
|
toStr,
|
|
each,
|
|
endWith,
|
|
contain,
|
|
filter,
|
|
isEmpty,
|
|
isArr,
|
|
isFn,
|
|
isRegExp,
|
|
uniqId,
|
|
last,
|
|
extend
|
|
} from './stringifyUtil'
|
|
|
|
// Modified from: https://jsconsole.com/
|
|
export default function stringify(
|
|
obj,
|
|
{
|
|
visitor = new Visitor(),
|
|
topObj,
|
|
level = 0,
|
|
getterVal = false,
|
|
unenumerable = true
|
|
} = {}
|
|
) {
|
|
let json = ''
|
|
let type
|
|
let parts = []
|
|
let names = []
|
|
let proto
|
|
let objAbstract
|
|
let circularObj
|
|
let allKeys
|
|
let keys
|
|
let id = ''
|
|
|
|
topObj = topObj || obj
|
|
|
|
let passOpts = { visitor, getterVal, unenumerable, level: level + 1 }
|
|
let passProtoOpts = {
|
|
visitor,
|
|
getterVal,
|
|
topObj,
|
|
unenumerable,
|
|
level: level + 1
|
|
}
|
|
|
|
let wrapKey = key => `"${escapeJsonStr(key)}"`
|
|
let wrapStr = str => `"${escapeJsonStr(toStr(str))}"`
|
|
|
|
type = getType(obj)
|
|
|
|
let isFn = type === '[object Function]'
|
|
let isStr = type === '[object String]'
|
|
let isArr = type === '[object Array]'
|
|
let isObj = type === '[object Object]'
|
|
let isNum = type === '[object Number]'
|
|
let isSymbol = type === '[object Symbol]'
|
|
let isBool = type === '[object Boolean]'
|
|
|
|
circularObj = visitor.check(obj)
|
|
|
|
if (circularObj) {
|
|
let abstract = circularObj.abstract
|
|
json = `{"erudaObjAbstract": ${wrapStr(
|
|
abstract.erudaObjAbstract
|
|
)}, "erudaCircular": ${wrapStr(abstract.erudaCircular)}}`
|
|
} else if (isStr) {
|
|
json = wrapStr(obj)
|
|
} else if (isArr || isObj || isFn) {
|
|
id = visitor.visit(obj)
|
|
|
|
if (canBeProto(obj)) {
|
|
obj = Object.getPrototypeOf(obj)
|
|
id = visitor.visit(obj)
|
|
}
|
|
|
|
names = getKeys(obj)
|
|
keys = names.keys
|
|
allKeys = names.allKeys
|
|
names = unenumerable ? allKeys : keys
|
|
|
|
proto = Object.getPrototypeOf(obj)
|
|
if (proto) {
|
|
proto = `${wrapKey('erudaProto')}: ${stringify(proto, passProtoOpts)}`
|
|
}
|
|
if (isFn) {
|
|
// We don't need these properties to display for functions.
|
|
names = names.filter(val => ['arguments', 'caller'].indexOf(val) < 0)
|
|
}
|
|
json = '{ '
|
|
objAbstract = getObjAbstract(obj)
|
|
visitor.updateAbstract(id, {
|
|
erudaObjAbstract: objAbstract,
|
|
erudaCircular: id
|
|
})
|
|
parts.push(`${wrapKey('erudaObjAbstract')}: ${wrapStr(objAbstract)}`)
|
|
parts.push(`"erudaId": "${id}"`)
|
|
each(names, objIteratee)
|
|
if (proto) parts.push(proto)
|
|
json += parts.join(', ') + ' }'
|
|
} else if (isNum) {
|
|
json = obj + ''
|
|
if (endWith(json, 'Infinity') || json === 'NaN') json = `"${json}"`
|
|
} else if (isBool) {
|
|
json = obj ? 'true' : 'false'
|
|
} else if (obj === null) {
|
|
json = 'null'
|
|
} else if (isSymbol) {
|
|
json = wrapStr('Symbol')
|
|
} else if (obj === undefined) {
|
|
json = wrapStr('undefined')
|
|
} else if (type === '[object HTMLAllCollection]') {
|
|
// https://docs.webplatform.org/wiki/dom/HTMLAllCollection
|
|
// Might cause a performance issue when stringify a dom element.
|
|
json = wrapStr('[object HTMLAllCollection]')
|
|
} else if (type === '[object HTMLDocument]' && level > 1) {
|
|
// Same as reason above.
|
|
json = wrapStr('[object HTMLDocument]')
|
|
} else {
|
|
try {
|
|
id = visitor.visit(obj)
|
|
if (canBeProto(obj)) {
|
|
obj = Object.getPrototypeOf(obj)
|
|
id = visitor.visit(obj)
|
|
}
|
|
|
|
json = '{ '
|
|
objAbstract = getObjAbstract(obj)
|
|
visitor.updateAbstract(id, {
|
|
erudaObjAbstract: objAbstract,
|
|
erudaCircular: id
|
|
})
|
|
parts.push(`${wrapKey('erudaObjAbstract')}: ${wrapStr(objAbstract)}`)
|
|
parts.push(`"erudaId": "${id}"`)
|
|
|
|
names = getKeys(obj)
|
|
keys = names.keys
|
|
allKeys = names.allKeys
|
|
names = unenumerable ? allKeys : keys
|
|
|
|
proto = Object.getPrototypeOf(obj)
|
|
if (proto) {
|
|
try {
|
|
proto = `${wrapKey('erudaProto')}: ${stringify(proto, passProtoOpts)}`
|
|
} catch (e) {
|
|
proto = `${wrapKey('erudaProto')}: ${wrapStr(e.message)}`
|
|
}
|
|
}
|
|
each(names, objIteratee)
|
|
if (proto) parts.push(proto)
|
|
json += parts.join(', ') + ' }'
|
|
} catch (e) {
|
|
json = wrapStr(obj)
|
|
}
|
|
}
|
|
|
|
function objIteratee(name) {
|
|
let unenumerable = !contain(keys, name) ? 'erudaUnenumerable ' : ''
|
|
let key = wrapKey(unenumerable + name)
|
|
let getKey = wrapKey(unenumerable + 'get ' + name)
|
|
let setKey = wrapKey(unenumerable + 'set ' + name)
|
|
|
|
let descriptor = Object.getOwnPropertyDescriptor(obj, name)
|
|
let hasGetter = descriptor && descriptor.get
|
|
let hasSetter = descriptor && descriptor.set
|
|
|
|
if (!getterVal && hasGetter) {
|
|
parts.push(`${key}: "(...)"`)
|
|
parts.push(`${getKey}: ${stringify(descriptor.get, passOpts)}`)
|
|
} else {
|
|
let val
|
|
try {
|
|
val = topObj[name]
|
|
} catch (e) {
|
|
val = e.message
|
|
}
|
|
parts.push(`${key}: ${stringify(val, passOpts)}`)
|
|
}
|
|
if (hasSetter) {
|
|
parts.push(`${setKey}: ${stringify(descriptor.set, passOpts)}`)
|
|
}
|
|
}
|
|
|
|
return json
|
|
}
|
|
|
|
function getKeys(obj) {
|
|
let allKeys = Object.getOwnPropertyNames(obj)
|
|
let keys = Object.keys(obj).sort(sortObjName)
|
|
|
|
allKeys = keys.concat(
|
|
filter(allKeys, val => !contain(keys, val)).sort(sortObjName)
|
|
)
|
|
|
|
return { keys, allKeys }
|
|
}
|
|
|
|
// $, upperCase, lowerCase, _
|
|
function sortObjName(a, b) {
|
|
let lenA = a.length
|
|
let lenB = b.length
|
|
let len = lenA > lenB ? lenB : lenA
|
|
|
|
for (let i = 0; i < len; i++) {
|
|
let codeA = a.charCodeAt(i)
|
|
let codeB = b.charCodeAt(i)
|
|
let cmpResult = cmpCode(codeA, codeB)
|
|
|
|
if (cmpResult !== 0) return cmpResult
|
|
}
|
|
|
|
if (lenA > lenB) return 1
|
|
if (lenA < lenB) return -1
|
|
|
|
return 0
|
|
}
|
|
|
|
function cmpCode(a, b) {
|
|
a = transCode(a)
|
|
b = transCode(b)
|
|
|
|
if (a > b) return 1
|
|
if (a < b) return -1
|
|
return 0
|
|
}
|
|
|
|
function transCode(code) {
|
|
// _ should be placed after lowercase chars.
|
|
if (code === 95) return 123
|
|
return code
|
|
}
|
|
|
|
let regFnHead = /function(.*?)\((.*?)\)/
|
|
|
|
function extractFnHead(fn) {
|
|
let str = fn.toString(),
|
|
fnHead = str.match(regFnHead)
|
|
|
|
if (fnHead) return fnHead[0]
|
|
|
|
return str
|
|
}
|
|
|
|
function getFnAbstract(fn) {
|
|
let fnStr = fn.toString()
|
|
if (fnStr.length > 500) fnStr = fnStr.slice(0, 500) + '...'
|
|
|
|
return extractFnHead(fnStr).replace('function', '')
|
|
}
|
|
|
|
function canBeProto(obj) {
|
|
let emptyObj = isEmpty(Object.getOwnPropertyNames(obj))
|
|
let proto = Object.getPrototypeOf(obj)
|
|
|
|
return emptyObj && proto && proto !== Object.prototype
|
|
}
|
|
|
|
function getObjAbstract(obj) {
|
|
if (isArr(obj)) return `Array(${obj.length})`
|
|
if (isFn(obj)) return getFnAbstract(obj)
|
|
if (isRegExp(obj)) return obj.toString()
|
|
|
|
let type = getType(obj)
|
|
|
|
return type.replace(/(\[object )|]/g, '')
|
|
}
|
|
|
|
function getType(obj) {
|
|
let type
|
|
|
|
try {
|
|
type = {}.toString.call(obj)
|
|
} catch (e) {
|
|
type = '[object Object]'
|
|
}
|
|
|
|
return type
|
|
}
|
|
|
|
class Visitor {
|
|
constructor() {
|
|
this._visited = []
|
|
this._map = {}
|
|
}
|
|
visit(val) {
|
|
/* Add 0 to distinguish stringify generated id from JsonViewer id.
|
|
* When used in web worker, they are not calling the same uniqId method, thus result may be repeated.
|
|
*/
|
|
let id = uniqId('erudaJson0')
|
|
|
|
this._visited.push({ id, val, abstract: {} })
|
|
this._map[id] = last(this._visited)
|
|
|
|
return id
|
|
}
|
|
check(val) {
|
|
let visited = this._visited
|
|
|
|
for (let i = 0, len = visited.length; i < len; i++) {
|
|
if (val === visited[i].val) return visited[i]
|
|
}
|
|
|
|
return false
|
|
}
|
|
update(id, data) {
|
|
extend(this._map[id], data)
|
|
}
|
|
updateAbstract(id, abstract) {
|
|
this.update(id, { abstract })
|
|
}
|
|
}
|