- // ==UserScript==
- // @name console-message-v2
- // @description Rich text console logging
- // @version 2.0.0
- // ==/UserScript==
-
- (self => {
-
- // 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
- }
-
- if (self) {
- if (self.console) {
- self.console.message = message
- }
-
- self.ConsoleMessage = ConsoleMessage
- }
-
- })(this || window)