Skip to content

Instantly share code, notes, and snippets.

@spiralx
Last active February 12, 2018 06:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save spiralx/fc040100134c5989eae30271cb715b1a to your computer and use it in GitHub Desktop.
Save spiralx/fc040100134c5989eae30271cb715b1a to your computer and use it in GitHub Desktop.
(function () {
const cssNumbers = new Set([
'column-count',
'fill-opacity',
'flex-grow',
'flex-shrink',
'font-weight',
'line-height',
'opacity',
'order',
'orphans',
'widows',
'z-index',
'zoom'
])
// --------------------------------------------------------------------
const support = (() => {
// Taken from https://github.com/jquery/jquery-migrate/blob/master/src/core.js
function uaMatch (ua) {
ua = ua.toLowerCase()
const 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'
}
}
const browserData = uaMatch(navigator.userAgent)
return {
isIE: browserData.browser === 'msie' || (browserData.browser === 'mozilla' && parseInt(browserData.version, 10) === 11)
}
})()
// --------------------------------------------------------------------
class ConsoleMessage {
constructor () {
this._rootSpan = {
styles: {},
children: [],
parent: null
}
this._currentSpan = this._rootSpan
this._waiting = 0
this._readyCallback = null
}
/**
* 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 (expanded) {
this._currentSpan.children.push({
type: expanded === false ? 'groupCollapsed' : 'group',
parent: this._currentSpan
})
return this
}
/**
* Ends the group and returns to writing to the parent message.
* @returns {ConsoleMessage} - Returns the message object itself to allow chaining.
*/
groupEnd () {
this._currentSpan.children.push({
type: 'groupEnd',
parent: this._currentSpan
})
return this
}
/**
* 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 span = {
type: 'span',
styles: Object.setPrototypeOf(styles, this._currentSpan.styles),
children: [],
parent: this._currentSpan
}
this._currentSpan.children.push(span)
this._currentSpan = 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 () {
this._currentSpan = this._currentSpan.parent || this._currentSpan
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 = {}) {
this.span(styles)
this._currentSpan.children.push({
type: 'text',
message,
parent: this._currentSpan
})
return this.spanEnd()
}
/**
* Adds a new line to the output.
* @returns {ConsoleMessage} - Returns the message object itself to allow chaining.
*/
line (type = 'log') {
this._currentSpan.children.push({
type,
parent: this._currentSpan
})
return this
}
/**
* 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) {
this._currentSpan.children.push({
type: 'element',
element,
parent: this._currentSpan
})
return this
}
/**
* 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) {
this._currentSpan.children.push({
type: 'object',
object,
parent: this._currentSpan
})
return this
}
dump (obj) {
try {
if (typeof obj.dump === 'function') {
obj.dump(this)
} else {
this.object(obj)
}
} catch (ex) {}
return this
}
/**
* Prints the message to the console.
* Until print() is called there will be no result to the console.
*/
print () {
if (typeof console !== 'undefined') {
const messages = [ this._newMessage() ]
this._printSpan(this._rootSpan, messages)
messages.forEach(message => {
if (message.text && message.text !== '%c' && console[ message.type ]) {
this._printMessage(message)
}
})
}
return new ConsoleMessage()
}
_printMessage (message) {
Function.prototype.apply.call(
console[ message.type ],
console,
[ message.text, ...message.args ]
)
}
_printSpan (span, messages) {
const message = messages[ messages.length - 1 ]
this._addSpanData(span, message)
span.children.forEach(child => {
this._handleChild(child, messages)
})
}
_handleChild (child, messages) {
let message = messages[ messages.length - 1 ]
switch (child.type) {
case 'group':
case 'groupCollapsed':
case 'log':
messages.push(this._newMessage(child.type))
break
case 'groupEnd':
message = this._newMessage('groupEnd', ' ')
messages.push(message)
messages.push(this._newMessage())
break
case 'span':
this._printSpan(child, messages)
this._addSpanData(child, message)
this._addSpanData(child.parent, message)
break
case 'text':
message.text += child.message
break
case 'element':
message.text += '%o'
message.args.push(child.element)
break
case 'object':
message.text += '%O'
message.args.push(child.object)
break
}
}
_addSpanData (span, message) {
if (!support.isIE) {
if (message.text.substring(message.text.length - 2) === '%c') {
message.args[ message.args.length - 1 ] = this._stylesString(span.styles)
} else {
message.text += '%c'
message.args.push(this._stylesString(span.styles))
}
}
}
_newMessage (type = 'log', text = '') {
return {
type,
text,
args: []
}
}
_stylesString (styles) {
return [ ...Object.entries(styles) ].map(([ property, value ]) => {
property = this._toDashName(property)
if (typeof value === 'number' && !cssNumbers.has(property)) {
value += 'px'
}
return `${property}: ${value};`
}).join(' ')
}
_toCamelCaseName (key) {
return key.replace(/-\w/g, match => match.charAt(1).toUpperCase())
}
_toDashName (key) {
return key.replace(/[A-Z]/g, match => '-' + match.toLowerCase())
}
}
if (typeof window !== 'undefined') {
if (!window.console) {
window.console = {}
}
/**
* Creates a message object.
* @returns {ConsoleMessage} - The message object
*/
window.console.message = () => new ConsoleMessage()
}
})()
/* jshint asi: true, laxbreak: true, esnext: true */
(function () {
const cssNumbers = new Set([
'column-count',
'fill-opacity',
'flex-grow',
'flex-shrink',
'font-weight',
'line-height',
'opacity',
'order',
'orphans',
'widows',
'z-index',
'zoom'
])
// --------------------------------------------------------------------
const support = (() => {
// Taken from https://github.com/jquery/jquery-migrate/blob/master/src/core.js
function uaMatch (ua) {
ua = ua.toLowerCase()
const 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'
}
}
const browserData = uaMatch(navigator.userAgent)
return {
isIE: browserData.browser === 'msie' || (browserData.browser === 'mozilla' && parseInt(browserData.version, 10) === 11)
}
})()
// --------------------------------------------------------------------
function toCamelCase (key) {
return key.replace(/-\w/g, match => match.charAt(1).toUpperCase())
}
function toKebabCase (key) {
return key.replace(/[A-Z]/g, match => '-' + match.toLowerCase())
}
function stylesToCss (styles) {
return [ ...Object.entries(styles) ].map(([ property, value ]) => {
property = toKebabCase(property)
if (typeof value === 'number' && !cssNumbers.has(property)) {
value += 'px'
}
return `${property}: ${value};`
}).join(' ')
}
// --------------------------------------------------------------------
class Item {
constructor (type, data = {}) {
this.type = type
this.parent = null
Object.assign(this, data)
}
write(messageList) {
messageList.add(new Message(this.type))
}
}
class SpanItem extends Item {
constructor(styles = {}) {
super('span')
this._styles = styles
this.children = []
}
push(item) {
item.parent = this
this.children.push(item)
return item
}
get styles() {
return {
...this.parent.styles,
...this._styles
}
}
get hasStyles() {
return Object.keys(this._styles).length > 0
}
get css() {
return stylesToCss(this.styles)
}
/*
pushStyle(messageList) {
if (this.hasStyles) {
messageList.current.pushStyle(this.css)
messageList._styleStack.push(this.css)
}
}
popStyle(messageList) {
if (this.hasStyles) {
messageList.current.pushStyle(this.css)
messageList._styleStack.push(this.css)
}
}*/
write(messageList) {
if (this.hasStyles) {
messageList.pushStyle(this.css)
}
this.children.forEach(child => {
child.write(messageList)
})
if (this.hasStyles) {
messageList.popStyle()
}
// messageList.current.pushStyle(stylesToCss(this.parent.styles))
}
}
class RootItem extends SpanItem {
constructor() {
super()
this.type = 'root'
}
get styles() {
return this._styles
}
get css() {
return ''
}
}
class TextItem extends Item {
constructor(message) {
super('text', { message })
}
write(messageList) {
messageList.current.append(this.message)
}
}
class LineItem extends Item {
constructor(type = 'log') {
super('line', { type })
}
write(messageList) {
messageList.add(new TextMessage(this.type))
}
}
class GroupItem extends Item {
constructor(collapsed = false) {
super('group', { collapsed })
}
write(messageList) {
messageList.add(new Message(this.collapsed ? 'groupCollapsed' : 'group'))
messageList.add(new TextMessage())
}
}
class GroupEndItem extends Item {
constructor() {
super('groupEnd')
}
write(messageList) {
messageList.add(new Message('groupEnd'))
messageList.add(new TextMessage())
}
}
class ElementItem extends Item {
constructor(element) {
super('element', { element })
}
write(messageList) {
messageList.current.append('%o', this.element)
}
}
class ObjectItem extends Item {
constructor(object) {
super('object', { object })
}
write(messageList) {
messageList.current.append('%O', this.object)
}
}
class TableItem extends Item {
constructor(array) {
super('table', { array })
}
write(messageList) {
messageList.add(new Message('table', this.array))
messageList.add(new TextMessage())
}
}
// --------------------------------------------------------------------
class MessageList {
constructor() {
this._items = [ new TextMessage() ]
this._styleStack = []
}
get current() {
return this._items[ this._items.length - 1]
}
pushStyle(css) {
this._styleStack.push(css)
this.current.pushStyle(css)
return css
}
popStyle() {
this._styleStack.pop()
const css = this._styleStack[ this._styleStack.length - 1 ]
this.current.pushStyle(css)
return css
}
add(message) {
this._items.push(message)
return message
}
* [ Symbol.iterator ]() {
yield* this._items
/*for (const message of this._items) {
yield message
}*/
}
}
class Message {
constructor (method, ...args) {
this.method = method
this.args = args
}
push(arg) {
this.args.push(arg)
}
execute() {
console[ this.method ].apply(console, this.args)
}
dump() {
console.log(`console.${this.method} (${'%o, '.repeat(this.args.length).slice(0, -2)})`, ...this.args)
}
}
class TextMessage extends Message {
constructor (method = 'log', text = '') {
super(method, text)
}
append (text, arg) {
this.text += text || ''
if (typeof arg !== 'undefined') {
super.push(arg)
}
}
pushStyle (css) {
if (this.text.endsWith('%c')) {
this.args[ this.args.length - 1 ] = css
} else {
this.append('%c', css)
}
}
get text () {
return this.args[ 0 ]
}
set text (value) {
this.args[ 0 ] = value
}
}
// --------------------------------------------------------------------
class ConsoleMessage {
constructor (debug = false) {
this._rootSpan = {
styles: {},
children: [],
parent: null
}
this._currentSpan = this._rootSpan
this._root = this._current = new RootItem()
this._waiting = 0
this._readyCallback = null
this._debug = debug
}
/**
* 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 (expanded) {
this._currentSpan.children.push({
type: expanded === false ? 'groupCollapsed' : 'group',
parent: this._currentSpan
})
this._current.push(new GroupItem(!expanded))
return this
}
/**
* Ends the group and returns to writing to the parent message.
* @returns {ConsoleMessage} - Returns the message object itself to allow chaining.
*/
groupEnd () {
this._currentSpan.children.push({
type: 'groupEnd',
parent: this._currentSpan
})
this._current.push(new GroupEndItem())
return this
}
/**
* 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 span = {
type: 'span',
styles: Object.setPrototypeOf(styles, this._currentSpan.styles),
children: [],
parent: this._currentSpan
}
this._currentSpan.children.push(span)
this._currentSpan = span
this._current = this._current.push(new SpanItem(styles))
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 () {
this._currentSpan = this._currentSpan.parent || this._currentSpan
this._current = this._current.parent || this._current
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 = null) {
if (styles !== null) {
this.span(styles)
}
this._currentSpan.children.push({
type: 'text',
message,
parent: this._currentSpan
})
this._current.push(new TextItem(message))
if (styles !== null) {
this.spanEnd()
}
return this
}
/**
* Adds a new line to the output.
* @returns {ConsoleMessage} - Returns the message object itself to allow chaining.
*/
line (type = 'log') {
this._currentSpan.children.push({
type,
parent: this._currentSpan
})
this._current.push(new LineItem(type))
return this
}
/**
* 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) {
this._currentSpan.children.push({
type: 'element',
element,
parent: this._currentSpan
})
this._current.push(new ElementItem(element))
return this
}
/**
* 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) {
if (typeof object.dump === 'function') {
object.dump(this)
} else {
this._currentSpan.children.push({
type: 'object',
object,
parent: this._currentSpan
})
this._current.push(new ObjectItem(object))
}
return this
}
trace () {
this._currentSpan.children.push({
type: 'trace',
parent: this._currentSpan
})
this._current.push(new Item('trace'))
return this
}
table (array) {
this._current.push(new TableItem(array))
return this
}
clear () {
this._current.push(new Item('clear'))
return this
}
/**
* Prints the message to the console.
* Until print() is called there will be no result to the console.
*/
print () {
if (typeof console !== 'undefined') {
if (this._debug) {
console.group('console.message')
console.group('_rootSpan')
console.dir(this._rootSpan)
console.groupEnd()
console.group('_root')
console.dir(this._root)
console.groupEnd()
}
const messages = [ this._newMessage() ]
this._printSpan(this._rootSpan, messages)
const messageList = new MessageList()
this._root.write(messageList)
if (this._debug) {
console.group('messages')
messages.forEach(message => {
const args = [ message.text, ...message.args ]
console.log(`console.${message.type}(${'%o, '.repeat(args.length).slice(0, -2)})`, ...args)
})
console.dir(messages)
console.groupEnd()
console.group('messageList')
for (const message of messageList) {
message.dump()
}
console.groupEnd()
console.groupEnd()
}
messages.forEach(message => {
if (message.text && message.text !== '%c' && console[ message.type ]) {
this._printMessage(message)
}
})
}
return new ConsoleMessage(this._debug)
}
_printMessage (message) {
Function.prototype.apply.call(
console[ message.type ],
console,
[ message.text, ...message.args ]
)
}
_printSpan (span, messages) {
const message = messages[ messages.length - 1 ]
this._addSpanData(span, message)
span.children.forEach(child => {
this._handleChild(child, messages)
})
}
_handleChild (child, messages) {
let message = messages[ messages.length - 1 ]
this._log(`_handleChild(child: %o, message.type: %s, message.args: %o)`, child, message.type, message.args)
switch (child.type) {
case 'groupEnd':
message = this._newMessage('groupEnd', ' ')
messages.push(message)
messages.push(this._newMessage())
break
case 'span':
this._printSpan(child, messages)
this._addSpanData(child, message)
this._addSpanData(child.parent, message)
break
case 'text':
message.text += child.message
break
case 'element':
message.text += '%o'
message.args.push(child.element)
break
case 'object':
message.text += '%O'
message.args.push(child.object)
break
case 'log':
case 'info':
case 'warn':
case 'error':
messages.push(this._newMessage(child.type))
break
case 'group':
case 'groupCollapsed':
messages.push(this._newMessage(child.type))
break
}
}
_addSpanData (span, message) {
const css = this._stylesString(span.styles)
this._log(`addSpanData(css: %s, message.type: %s, message.args: %o)`, span, message.type, message.args)
if (!support.isIE) {
if (message.text.endsWith('%c')) {
message.args[ message.args.length - 1 ] = css
} else {
message.text += '%c'
message.args.push(css)
}
}
}
_newMessage (type = 'log', text = '') {
return {
type,
text,
args: []
}
}
_stylesString (styles) {
return [ ...Object.entries(styles) ].map(([ property, value ]) => {
property = this._toDashName(property)
if (typeof value === 'number' && !cssNumbers.has(property)) {
value += 'px'
}
return `${property}: ${value};`
}).join(' ')
}
_toCamelCaseName (key) {
return key.replace(/-\w/g, match => match.charAt(1).toUpperCase())
}
_toDashName (key) {
return key.replace(/[A-Z]/g, match => '-' + match.toLowerCase())
}
_log (...args) {
if (this._debug) {
console.log(...args)
}
}
}
if (typeof window !== 'undefined') {
if (!window.console) {
window.console = {}
}
/**
* Creates a message object.
* @returns {ConsoleMessage} - The message object
*/
window.console.message = (debug = false) => new ConsoleMessage(debug)
}
})()
/**
let fs = {
fontWeight: 'bold',
backgroundColor: '#5af'
}
let t = {
text: 'Woohoo',
backgroundColor: 'yellow',
dump(msg) {
return msg.text(this.text, { backgroundColor: this.backgroundColor })
}
}
console.clear()
console.message(true)
.text('thing: ', { fontWeight: 'bold' })
.object(t)
.line()
.span({ padding: '2px 4px', borderRadius: 4 })
.text('foo', { backgroundColor: '#05f', color: 'white' })
.text('bar', { backgroundColor: '#0f0' })
.spanEnd()
.text(' and cows')
.print()
.group()
.text('trace:')
.line()
.trace()
.groupEnd()
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment