console-message-v2

Rich text console logging

目前为 2019-09-04 提交的版本。查看 最新版本

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/389748/730534/console-message-v2.js

  1. // ==UserScript==
  2. // @name console-message-v2
  3. // @description Rich text console logging
  4. // @version 2.0.0
  5. // ==/UserScript==
  6.  
  7. (self => {
  8.  
  9. // Properties whose values have a CSS type
  10. // of either <integer> or <number>
  11. const CSS_NUMERIC_PROPERTIES = new Set([
  12. 'animation-iteration-count',
  13. 'border-image-slice',
  14. 'border-image-outset',
  15. 'border-image-width',
  16. 'column-count',
  17. 'counter-increment',
  18. 'fill-opacity',
  19. 'flex-grow',
  20. 'flex-shrink',
  21. 'font-size-adjust',
  22. 'font-weight',
  23. 'grid-column',
  24. 'grid-row',
  25. 'initial-letters',
  26. 'line-clamp',
  27. 'line-height',
  28. 'max-lines',
  29. 'opacity',
  30. 'order',
  31. 'orphans',
  32. 'stop-opacity',
  33. 'stroke-dashoffset',
  34. 'stroke-miterlimit',
  35. 'stroke-opacity',
  36. 'stroke-width',
  37. 'tab-size',
  38. 'widows',
  39. 'z-index',
  40. 'zoom'
  41. ])
  42.  
  43. const _SIMPLE_METHODS = new Set(['groupEnd', 'trace'])
  44. const _TEXT_METHODS = new Set(['log', 'info', 'warn', 'error'])
  45.  
  46. // ----------------------------------------------------------
  47.  
  48. // const toCamelCase = s => s.replace(/-\w/g, match => match.charAt(1).toUpperCase())
  49. const toKebabCase = s => s.replace(/[A-Z]/g, match => '-' + match.toLowerCase())
  50.  
  51. function getStyleForCssProps(cssProps) {
  52. return Object.entries(cssProps)
  53. .map(([ key, value ]) => {
  54. const cssProp = toKebabCase(key)
  55.  
  56. if (typeof value === 'number' && !CSS_NUMERIC_PROPERTIES.has(cssProp)) {
  57. value = value + 'px'
  58. }
  59.  
  60. return cssProp + ': ' + value
  61. })
  62. .join('; ')
  63. }
  64.  
  65. // ----------------------------------------------------------
  66.  
  67. var support = (function () {
  68. // Taken from https://github.com/jquery/jquery-migrate/blob/master/src/core.js
  69. function uaMatch(ua) {
  70. ua = ua.toLowerCase()
  71.  
  72. var match = /(chrome)[ \/]([\w.]+)/.exec(ua) ||
  73. /(webkit)[ \/]([\w.]+)/.exec(ua) ||
  74. /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) ||
  75. /(msie) ([\w.]+)/.exec(ua) ||
  76. ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || []
  77.  
  78. return {
  79. browser: match[1] || "",
  80. version: match[2] || "0"
  81. }
  82. }
  83. var browserData = uaMatch(navigator.userAgent)
  84.  
  85. return {
  86. isIE: browserData.browser == 'msie' || (browserData.browser == 'mozilla' && parseInt(browserData.version, 10) == 11)
  87. }
  88. })()
  89.  
  90. // ----------------------------------------------------------
  91.  
  92. class ConsoleMessage {
  93. /** @type ConsoleMessage | null */
  94. static _currentMessage = null
  95.  
  96. static debug = false
  97.  
  98. // ----------------------------------------
  99.  
  100. constructor () {
  101. this._rootNode = {
  102. type: 'root',
  103. parentNode: null,
  104. styles: {},
  105. children: []
  106. }
  107.  
  108. this._currentParent = this._rootNode
  109.  
  110. this._calls = new MethodCalls()
  111.  
  112. this._waiting = 0
  113. this._readyCallback = null
  114. }
  115.  
  116. // ----------------------------------------
  117.  
  118. extend(obj = {}) {
  119. const proto = Object.getPrototypeOf(this)
  120. Object.assign(proto, obj)
  121. return this
  122. }
  123.  
  124. // ----------------------------------------
  125.  
  126. /**
  127. * Begins a group. By default the group is expanded. Provide false if you want the group to be collapsed.
  128. * @param {boolean} [expanded = true] -
  129. * @returns {ConsoleMessage} - Returns the message object itself to allow chaining.
  130. */
  131. group (collapsed = false) {
  132. return this._appendNode(collapsed ? 'groupCollapsed' : 'group')
  133. }
  134.  
  135. // ----------------------------------------
  136.  
  137. /**
  138. * Ends the group and returns to writing to the parent message.
  139. * @returns {ConsoleMessage} - Returns the message object itself to allow chaining.
  140. */
  141. groupEnd () {
  142. return this._appendNode('groupEnd')
  143. }
  144.  
  145. // ----------------------------------------
  146.  
  147. /**
  148. * Starts a span with particular style and all appended text after it will use the style.
  149. * @param {Object} styles - The CSS styles to be applied to all text until endSpan() is called
  150. * @returns {ConsoleMessage} - Returns the message object itself to allow chaining.
  151. */
  152. span (styles = {}) {
  153. const parentNode = this._currentParent
  154.  
  155. const span = {
  156. type: 'span',
  157. parentNode,
  158. previousStyles: {
  159. ...parentNode.styles
  160. },
  161. styles: {
  162. ...parentNode.styles,
  163. ...styles
  164. },
  165. children: []
  166. }
  167.  
  168. parentNode.children.push(span)
  169.  
  170. this._currentParent = span
  171.  
  172. return this
  173. }
  174.  
  175. // ----------------------------------------
  176.  
  177. /**
  178. * Ends the current span styles and backs to the previous styles or the root if there are no other parents.
  179. * @returns {ConsoleMessage} - Returns the message object itself to allow chaining.
  180. */
  181. spanEnd () {
  182. if (this._currentParent.type === 'root') {
  183. throw new Error(`Cannot call spanEnd() without a span`)
  184. }
  185.  
  186. this._currentParent = this._currentParent.parentNode
  187. return this
  188. }
  189.  
  190. // ----------------------------------------
  191.  
  192. /**
  193. * Appends a text to the current message. All styles in the current span are applied.
  194. * @param {string} text - The text to be appended
  195. * @returns {ConsoleMessage} - Returns the message object itself to allow chaining.
  196. */
  197. text (message, styles) {
  198. if (typeof styles !== 'object') {
  199. return this._appendNode('text', { message })
  200. }
  201.  
  202. this.span(styles)
  203.  
  204. this._appendNode('text', { message })
  205.  
  206. this.spanEnd()
  207.  
  208. return this
  209. }
  210.  
  211. // ----------------------------------------
  212.  
  213. /**
  214. * Adds a new line to the output.
  215. * @returns {ConsoleMessage} - Returns the message object itself to allow chaining.
  216. */
  217. line (method = 'log') {
  218. return this._appendNode('line', { method })
  219. }
  220.  
  221. // ----------------------------------------
  222.  
  223. /**
  224. * Adds an interactive DOM element to the output.
  225. * @param {HTMLElement} element - The DOM element to be added.
  226. * @returns {ConsoleMessage} - Returns the message object itself to allow chaining.
  227. */
  228. element (element) {
  229. return this._appendNode('element', { element })
  230. }
  231.  
  232. // ----------------------------------------
  233.  
  234. /**
  235. * Adds an interactive object tree to the output.
  236. * @param {*} object - A value to be added to the output.
  237. * @returns {ConsoleMessage} - Returns the message object itself to allow chaining.
  238. */
  239. object (object) {
  240. return this._appendNode('object', { object })
  241. }
  242.  
  243. // ----------------------------------------
  244.  
  245. /**
  246. * Adds an error message and stack trace to the output.
  247. * @param {Error} error - An error to be added to the output.
  248. * @returns {ConsoleMessage} - Returns the message object itself to allow chaining.
  249. */
  250. error (error) {
  251. return this._appendNode('error', { error })
  252. }
  253.  
  254. // ----------------------------------------
  255.  
  256. /**
  257. * Write a stack trace to the console.
  258. * @returns {ConsoleMessage} - Returns the message object itself to allow chaining.
  259. */
  260. trace() {
  261. return this._appendNode('trace')
  262. }
  263.  
  264. // ----------------------------------------
  265.  
  266. _appendNode (type, props = {}) {
  267. const child = {
  268. type,
  269. parentNode: this._currentParent,
  270. ...props
  271. }
  272.  
  273. this._currentParent.children.push(child)
  274.  
  275. return this
  276. }
  277.  
  278. // ----------------------------------------
  279.  
  280. /**
  281. * Prints the message to the console.
  282. * Until print() is called there will be no result to the console.
  283. */
  284. print () {
  285. if (ConsoleMessage.debug) {
  286. console.group(`this._rootNode`)
  287. console.dir(this._rootNode)
  288. }
  289.  
  290. try {
  291. this._generateMethodCalls(this._rootNode)
  292.  
  293. if (ConsoleMessage.debug) {
  294. console.dir(this._calls)
  295. console.groupEnd()
  296. }
  297.  
  298. this._calls.executeAll()
  299. } catch (ex) {
  300. console.warn(ex)
  301.  
  302. if (ConsoleMessage.debug) {
  303. console.groupEnd()
  304. }
  305. }
  306.  
  307. ConsoleMessage._currentMessage = null
  308. }
  309.  
  310. // ----------------------------------------
  311.  
  312. _generateMethodCalls (node) {
  313. const calls = this._calls
  314.  
  315. switch (node.type) {
  316. case 'root':
  317. case 'span':
  318. calls.appendStyle(node.styles)
  319.  
  320. for (const child of node.children) {
  321. this._generateMethodCalls(child)
  322. }
  323.  
  324. if (node.previousStyles != null) {
  325. calls.appendStyle(node.previousStyles)
  326. }
  327.  
  328. break
  329.  
  330. case 'line':
  331. calls
  332. .addCall(node.method)
  333. break
  334.  
  335. case 'group':
  336. case 'groupCollapsed':
  337. case 'groupEnd':
  338. case 'trace':
  339. calls
  340. .addCall(node.type)
  341. .addCall()
  342. break
  343.  
  344. case 'text':
  345. calls
  346. .appendText(node.message)
  347. break
  348.  
  349. case 'element':
  350. calls
  351. .appendText('%o')
  352. .addArg(node.element)
  353. break
  354.  
  355. case 'object':
  356. calls
  357. .appendText('%O')
  358. .addArg(node.object)
  359. break
  360.  
  361. case 'error':
  362. calls
  363. .addCall('error')
  364. .addArg(node.error)
  365. .addCall()
  366. break
  367. }
  368. }
  369. }
  370.  
  371. // ----------------------------------------------------------
  372.  
  373. class MethodCalls {
  374. constructor () {
  375. this.methodCalls = [
  376. new MethodCall('log')
  377. ]
  378. }
  379.  
  380. // ----------------------------------------
  381.  
  382. get currentCall () {
  383. return this.methodCalls[ this.methodCalls.length - 1 ]
  384. }
  385.  
  386. // ----------------------------------------
  387.  
  388. addCall (methodName = 'log') {
  389. if (_TEXT_METHODS.has(methodName) && this.currentCall.canChangeMethod) {
  390. this.currentCall.changeMethod(methodName)
  391. } else {
  392. this.methodCalls.push(new MethodCall(methodName))
  393. }
  394.  
  395. return this
  396. }
  397.  
  398. // ----------------------------------------
  399.  
  400. appendText (text) {
  401. this.currentCall.appendText(text)
  402. return this
  403. }
  404.  
  405. // ----------------------------------------
  406.  
  407. appendStyle (cssProps) {
  408. this.currentCall.appendStyle(cssProps)
  409. return this
  410. }
  411.  
  412. // ----------------------------------------
  413.  
  414. addArg (arg) {
  415. this.currentCall.addArg(arg)
  416. return this
  417. }
  418.  
  419. // ----------------------------------------
  420.  
  421. executeAll () {
  422. this.methodCalls
  423. .filter(c => c.canExecute)
  424. .forEach(c => {
  425. c.execute()
  426. })
  427. }
  428. }
  429.  
  430. // ----------------------------------------------------------
  431.  
  432. class MethodCall {
  433. constructor (methodName = 'log') {
  434. this.methodName = methodName
  435.  
  436. this.isSimple = _SIMPLE_METHODS.has(methodName)
  437. this.canChangeMethod = _TEXT_METHODS.has(methodName)
  438.  
  439. this.text = ''
  440. this.args = []
  441. }
  442.  
  443. // ----------------------------------------
  444.  
  445. get lastArg () {
  446. return this.args[ this.args.length - 1 ]
  447. }
  448.  
  449. // ----------------------------------------
  450.  
  451. changeMethod (newMethodName) {
  452. if (!this.canChangeMethod) {
  453. throw new Error(`Cannot change method call`)
  454. }
  455.  
  456. if (!_TEXT_METHODS.has(newMethodName)) {
  457. throw new Error(`Cannot change method call to "${newMethodName}"`)
  458. }
  459.  
  460. this.methodName = newMethodName
  461. this.canChangeMethod = false
  462. }
  463.  
  464. // ----------------------------------------
  465.  
  466. _throwIfSimple () {
  467. if (this.isSimple) {
  468. throw new Error(`Calls to console.${this.methodName} cannot have arguments`)
  469. }
  470. }
  471.  
  472. // ----------------------------------------
  473.  
  474. appendText (text, styles) {
  475. this._throwIfSimple()
  476.  
  477. this.text += text
  478. this.canChangeMethod = false
  479.  
  480. return this
  481. }
  482.  
  483. // ----------------------------------------
  484.  
  485. appendStyle (cssProps) {
  486. this._throwIfSimple()
  487.  
  488. const css = getStyleForCssProps(cssProps)
  489.  
  490. if (this.text.endsWith('%c')) {
  491. this.args[ this.args.length - 1 ] = css
  492. } else {
  493. this.text += '%c'
  494. this.args.push(css)
  495. }
  496.  
  497. this.canChangeMethod = false
  498.  
  499. return this
  500. }
  501.  
  502. // ----------------------------------------
  503.  
  504. // updateLastArg (newValue) {
  505. // if (this.args.length) {
  506. // this.args[ this.args.length ] = newValue
  507. // }
  508. // return this
  509. // }
  510.  
  511. // ----------------------------------------
  512.  
  513. addArg (newArg) {
  514. this._throwIfSimple()
  515.  
  516. this.args.push(newArg)
  517. this.canChangeMethod = false
  518.  
  519. return this
  520. }
  521.  
  522. // ----------------------------------------
  523.  
  524. get canExecute () {
  525. return this.isSimple
  526. ? true
  527. : this.text.length > 0 && this.text !== '%c'
  528. }
  529.  
  530. // ----------------------------------------
  531.  
  532. execute () {
  533. const method = console[this.methodName].bind(console)
  534. method(this.text, ...this.args)
  535. return this
  536. }
  537. }
  538.  
  539. // ----------------------------------------------------------
  540.  
  541. // console.message().text('woo').text('blue',{color:'#05f'}).line('info').element(document.body).print()
  542.  
  543. /**
  544. * Returns or creates the current message object.
  545. *
  546. * @returns {ConsoleMessage} - The message object
  547. */
  548. function message() {
  549. if (ConsoleMessage._currentMessage === null) {
  550. ConsoleMessage._currentMessage = new ConsoleMessage()
  551. }
  552.  
  553. return ConsoleMessage._currentMessage
  554. }
  555.  
  556. if (self) {
  557. if (self.console) {
  558. self.console.message = message
  559. }
  560.  
  561. self.ConsoleMessage = ConsoleMessage
  562. }
  563.  
  564. })(this || window)