Created
September 4, 2019 04:09
-
-
Save spiralx/91945fe5f0597fc3cd20e2292a74932d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Properties whose values have a CSS type | |
// of either <integer> or <number> | |
const CSS_NUMERIC_PROPERTIES = new Set([ | |
'animation-iteration-count', | |
'border-image-slice', | |
'border-image-outset', | |
'border-image-width', | |
'column-count', | |
'counter-increment', | |
'fill-opacity', | |
'flex-grow', | |
'flex-shrink', | |
'font-size-adjust', | |
'font-weight', | |
'grid-column', | |
'grid-row', | |
'initial-letters', | |
'line-clamp', | |
'line-height', | |
'max-lines', | |
'opacity', | |
'order', | |
'orphans', | |
'stop-opacity', | |
'stroke-dashoffset', | |
'stroke-miterlimit', | |
'stroke-opacity', | |
'stroke-width', | |
'tab-size', | |
'widows', | |
'z-index', | |
'zoom' | |
]) | |
const _SIMPLE_METHODS = new Set(['groupEnd', 'trace']) | |
const _TEXT_METHODS = new Set(['log', 'info', 'warn', 'error']) | |
// ---------------------------------------------------------- | |
// const toCamelCase = s => s.replace(/-\w/g, match => match.charAt(1).toUpperCase()) | |
const toKebabCase = s => s.replace(/[A-Z]/g, match => '-' + match.toLowerCase()) | |
function getStyleForCssProps(cssProps) { | |
return Object.entries(cssProps) | |
.map(([ key, value ]) => { | |
const cssProp = toKebabCase(key) | |
if (typeof value === 'number' && !CSS_NUMERIC_PROPERTIES.has(cssProp)) { | |
value = value + 'px' | |
} | |
return cssProp + ': ' + value | |
}) | |
.join('; ') | |
} | |
// ---------------------------------------------------------- | |
var support = (function () { | |
// Taken from https://github.com/jquery/jquery-migrate/blob/master/src/core.js | |
function uaMatch(ua) { | |
ua = ua.toLowerCase() | |
var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || | |
/(webkit)[ \/]([\w.]+)/.exec(ua) || | |
/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || | |
/(msie) ([\w.]+)/.exec(ua) || | |
ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || [] | |
return { | |
browser: match[1] || "", | |
version: match[2] || "0" | |
} | |
} | |
var browserData = uaMatch(navigator.userAgent) | |
return { | |
isIE: browserData.browser == 'msie' || (browserData.browser == 'mozilla' && parseInt(browserData.version, 10) == 11) | |
} | |
})() | |
// ---------------------------------------------------------- | |
class ConsoleMessage { | |
/** @type ConsoleMessage | null */ | |
static _currentMessage = null | |
static debug = false | |
// ---------------------------------------- | |
constructor () { | |
this._rootNode = { | |
type: 'root', | |
parentNode: null, | |
styles: {}, | |
children: [] | |
} | |
this._currentParent = this._rootNode | |
this._calls = new MethodCalls() | |
this._waiting = 0 | |
this._readyCallback = null | |
} | |
// ---------------------------------------- | |
extend(obj = {}) { | |
const proto = Object.getPrototypeOf(this) | |
Object.assign(proto, obj) | |
return this | |
} | |
// ---------------------------------------- | |
/** | |
* Begins a group. By default the group is expanded. Provide false if you want the group to be collapsed. | |
* @param {boolean} [expanded = true] - | |
* @returns {ConsoleMessage} - Returns the message object itself to allow chaining. | |
*/ | |
group (collapsed = false) { | |
return this._appendNode(collapsed ? 'groupCollapsed' : 'group') | |
} | |
// ---------------------------------------- | |
/** | |
* Ends the group and returns to writing to the parent message. | |
* @returns {ConsoleMessage} - Returns the message object itself to allow chaining. | |
*/ | |
groupEnd () { | |
return this._appendNode('groupEnd') | |
} | |
// ---------------------------------------- | |
/** | |
* Starts a span with particular style and all appended text after it will use the style. | |
* @param {Object} styles - The CSS styles to be applied to all text until endSpan() is called | |
* @returns {ConsoleMessage} - Returns the message object itself to allow chaining. | |
*/ | |
span (styles = {}) { | |
const parentNode = this._currentParent | |
const span = { | |
type: 'span', | |
parentNode, | |
previousStyles: { | |
...parentNode.styles | |
}, | |
styles: { | |
...parentNode.styles, | |
...styles | |
}, | |
children: [] | |
} | |
parentNode.children.push(span) | |
this._currentParent = span | |
return this | |
} | |
// ---------------------------------------- | |
/** | |
* Ends the current span styles and backs to the previous styles or the root if there are no other parents. | |
* @returns {ConsoleMessage} - Returns the message object itself to allow chaining. | |
*/ | |
spanEnd () { | |
if (this._currentParent.type === 'root') { | |
throw new Error(`Cannot call spanEnd() without a span`) | |
} | |
this._currentParent = this._currentParent.parentNode | |
return this | |
} | |
// ---------------------------------------- | |
/** | |
* Appends a text to the current message. All styles in the current span are applied. | |
* @param {string} text - The text to be appended | |
* @returns {ConsoleMessage} - Returns the message object itself to allow chaining. | |
*/ | |
text (message, styles) { | |
if (typeof styles !== 'object') { | |
return this._appendNode('text', { message }) | |
} | |
this.span(styles) | |
this._appendNode('text', { message }) | |
this.spanEnd() | |
return this | |
} | |
// ---------------------------------------- | |
/** | |
* Adds a new line to the output. | |
* @returns {ConsoleMessage} - Returns the message object itself to allow chaining. | |
*/ | |
line (method = 'log') { | |
return this._appendNode('line', { method }) | |
} | |
// ---------------------------------------- | |
/** | |
* Adds an interactive DOM element to the output. | |
* @param {HTMLElement} element - The DOM element to be added. | |
* @returns {ConsoleMessage} - Returns the message object itself to allow chaining. | |
*/ | |
element (element) { | |
return this._appendNode('element', { element }) | |
} | |
// ---------------------------------------- | |
/** | |
* Adds an interactive object tree to the output. | |
* @param {*} object - A value to be added to the output. | |
* @returns {ConsoleMessage} - Returns the message object itself to allow chaining. | |
*/ | |
object (object) { | |
return this._appendNode('object', { object }) | |
} | |
// ---------------------------------------- | |
/** | |
* Adds an error message and stack trace to the output. | |
* @param {Error} error - An error to be added to the output. | |
* @returns {ConsoleMessage} - Returns the message object itself to allow chaining. | |
*/ | |
error (error) { | |
return this._appendNode('error', { error }) | |
} | |
// ---------------------------------------- | |
/** | |
* Write a stack trace to the console. | |
* @returns {ConsoleMessage} - Returns the message object itself to allow chaining. | |
*/ | |
trace() { | |
return this._appendNode('trace') | |
} | |
// ---------------------------------------- | |
_appendNode (type, props = {}) { | |
const child = { | |
type, | |
parentNode: this._currentParent, | |
...props | |
} | |
this._currentParent.children.push(child) | |
return this | |
} | |
// ---------------------------------------- | |
/** | |
* Prints the message to the console. | |
* Until print() is called there will be no result to the console. | |
*/ | |
print () { | |
if (ConsoleMessage.debug) { | |
console.group(`this._rootNode`) | |
console.dir(this._rootNode) | |
} | |
try { | |
this._generateMethodCalls(this._rootNode) | |
if (ConsoleMessage.debug) { | |
console.dir(this._calls) | |
console.groupEnd() | |
} | |
this._calls.executeAll() | |
} catch (ex) { | |
console.warn(ex) | |
if (ConsoleMessage.debug) { | |
console.groupEnd() | |
} | |
} | |
ConsoleMessage._currentMessage = null | |
} | |
// ---------------------------------------- | |
_generateMethodCalls (node) { | |
const calls = this._calls | |
switch (node.type) { | |
case 'root': | |
case 'span': | |
calls.appendStyle(node.styles) | |
for (const child of node.children) { | |
this._generateMethodCalls(child) | |
} | |
if (node.previousStyles != null) { | |
calls.appendStyle(node.previousStyles) | |
} | |
break | |
case 'line': | |
calls | |
.addCall(node.method) | |
break | |
case 'group': | |
case 'groupCollapsed': | |
case 'groupEnd': | |
case 'trace': | |
calls | |
.addCall(node.type) | |
.addCall() | |
break | |
case 'text': | |
calls | |
.appendText(node.message) | |
break | |
case 'element': | |
calls | |
.appendText('%o') | |
.addArg(node.element) | |
break | |
case 'object': | |
calls | |
.appendText('%O') | |
.addArg(node.object) | |
break | |
case 'error': | |
calls | |
.addCall('error') | |
.addArg(node.error) | |
.addCall() | |
break | |
} | |
} | |
} | |
// ---------------------------------------------------------- | |
class MethodCalls { | |
constructor () { | |
this.methodCalls = [ | |
new MethodCall('log') | |
] | |
} | |
// ---------------------------------------- | |
get currentCall () { | |
return this.methodCalls[ this.methodCalls.length - 1 ] | |
} | |
// ---------------------------------------- | |
addCall (methodName = 'log') { | |
if (_TEXT_METHODS.has(methodName) && this.currentCall.canChangeMethod) { | |
this.currentCall.changeMethod(methodName) | |
} else { | |
this.methodCalls.push(new MethodCall(methodName)) | |
} | |
return this | |
} | |
// ---------------------------------------- | |
appendText (text) { | |
this.currentCall.appendText(text) | |
return this | |
} | |
// ---------------------------------------- | |
appendStyle (cssProps) { | |
this.currentCall.appendStyle(cssProps) | |
return this | |
} | |
// ---------------------------------------- | |
addArg (arg) { | |
this.currentCall.addArg(arg) | |
return this | |
} | |
// ---------------------------------------- | |
executeAll () { | |
this.methodCalls | |
.filter(c => c.canExecute) | |
.forEach(c => { | |
c.execute() | |
}) | |
} | |
} | |
// ---------------------------------------------------------- | |
class MethodCall { | |
constructor (methodName = 'log') { | |
this.methodName = methodName | |
this.isSimple = _SIMPLE_METHODS.has(methodName) | |
this.canChangeMethod = _TEXT_METHODS.has(methodName) | |
this.text = '' | |
this.args = [] | |
} | |
// ---------------------------------------- | |
get lastArg () { | |
return this.args[ this.args.length - 1 ] | |
} | |
// ---------------------------------------- | |
changeMethod (newMethodName) { | |
if (!this.canChangeMethod) { | |
throw new Error(`Cannot change method call`) | |
} | |
if (!_TEXT_METHODS.has(newMethodName)) { | |
throw new Error(`Cannot change method call to "${newMethodName}"`) | |
} | |
this.methodName = newMethodName | |
this.canChangeMethod = false | |
} | |
// ---------------------------------------- | |
_throwIfSimple () { | |
if (this.isSimple) { | |
throw new Error(`Calls to console.${this.methodName} cannot have arguments`) | |
} | |
} | |
// ---------------------------------------- | |
appendText (text, styles) { | |
this._throwIfSimple() | |
this.text += text | |
this.canChangeMethod = false | |
return this | |
} | |
// ---------------------------------------- | |
appendStyle (cssProps) { | |
this._throwIfSimple() | |
const css = getStyleForCssProps(cssProps) | |
if (this.text.endsWith('%c')) { | |
this.args[ this.args.length - 1 ] = css | |
} else { | |
this.text += '%c' | |
this.args.push(css) | |
} | |
this.canChangeMethod = false | |
return this | |
} | |
// ---------------------------------------- | |
// updateLastArg (newValue) { | |
// if (this.args.length) { | |
// this.args[ this.args.length ] = newValue | |
// } | |
// return this | |
// } | |
// ---------------------------------------- | |
addArg (newArg) { | |
this._throwIfSimple() | |
this.args.push(newArg) | |
this.canChangeMethod = false | |
return this | |
} | |
// ---------------------------------------- | |
get canExecute () { | |
return this.isSimple | |
? true | |
: this.text.length > 0 && this.text !== '%c' | |
} | |
// ---------------------------------------- | |
execute () { | |
const method = console[this.methodName].bind(console) | |
method(this.text, ...this.args) | |
return this | |
} | |
} | |
// ---------------------------------------------------------- | |
// console.message().text('woo').text('blue',{color:'#05f'}).line('info').element(document.body).print() | |
/** | |
* Returns or creates the current message object. | |
* | |
* @returns {ConsoleMessage} - The message object | |
*/ | |
function message() { | |
if (ConsoleMessage._currentMessage === null) { | |
ConsoleMessage._currentMessage = new ConsoleMessage() | |
} | |
return ConsoleMessage._currentMessage | |
} | |
console.message = message | |
console.ConsoleMessage = ConsoleMessage |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment