format-package-stream.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. // XXX these output classes should not live in here forever. it'd be good to
  2. // split them out, perhaps to libnpmsearch
  3. const Minipass = require('minipass')
  4. const columnify = require('columnify')
  5. // This module consumes package data in the following format:
  6. //
  7. // {
  8. // name: String,
  9. // description: String,
  10. // maintainers: [{ username: String, email: String }],
  11. // keywords: String | [String],
  12. // version: String,
  13. // date: Date // can be null,
  14. // }
  15. //
  16. // The returned stream will format this package data
  17. // into a byte stream of formatted, displayable output.
  18. module.exports = (opts = {}) =>
  19. opts.json ? new JSONOutputStream() : new TextOutputStream(opts)
  20. class JSONOutputStream extends Minipass {
  21. constructor () {
  22. super()
  23. this._didFirst = false
  24. }
  25. write (obj) {
  26. if (!this._didFirst) {
  27. super.write('[\n')
  28. this._didFirst = true
  29. } else
  30. super.write('\n,\n')
  31. try {
  32. return super.write(JSON.stringify(obj))
  33. } catch (er) {
  34. return this.emit('error', er)
  35. }
  36. }
  37. end () {
  38. super.write(this._didFirst ? ']\n' : '\n[]\n')
  39. super.end()
  40. }
  41. }
  42. class TextOutputStream extends Minipass {
  43. constructor (opts) {
  44. super()
  45. this._opts = opts
  46. this._line = 0
  47. }
  48. write (pkg) {
  49. return super.write(prettify(pkg, ++this._line, this._opts))
  50. }
  51. }
  52. function prettify (data, num, opts) {
  53. opts = opts || {}
  54. var truncate = !opts.long
  55. var pkg = normalizePackage(data, opts)
  56. var columns = opts.description
  57. ? ['name', 'description', 'author', 'date', 'version', 'keywords']
  58. : ['name', 'author', 'date', 'version', 'keywords']
  59. if (opts.parseable) {
  60. return columns.map(function (col) {
  61. return pkg[col] && ('' + pkg[col]).replace(/\t/g, ' ')
  62. }).join('\t')
  63. }
  64. var output = columnify(
  65. [pkg],
  66. {
  67. include: columns,
  68. showHeaders: num <= 1,
  69. columnSplitter: ' | ',
  70. truncate: truncate,
  71. config: {
  72. name: { minWidth: 25, maxWidth: 25, truncate: false, truncateMarker: '' },
  73. description: { minWidth: 20, maxWidth: 20 },
  74. author: { minWidth: 15, maxWidth: 15 },
  75. date: { maxWidth: 11 },
  76. version: { minWidth: 8, maxWidth: 8 },
  77. keywords: { maxWidth: Infinity },
  78. },
  79. }
  80. )
  81. output = trimToMaxWidth(output)
  82. if (opts.color)
  83. output = highlightSearchTerms(output, opts.args)
  84. return output
  85. }
  86. var colors = [31, 33, 32, 36, 34, 35]
  87. var cl = colors.length
  88. function addColorMarker (str, arg, i) {
  89. var m = i % cl + 1
  90. var markStart = String.fromCharCode(m)
  91. var markEnd = String.fromCharCode(0)
  92. if (arg.charAt(0) === '/') {
  93. return str.replace(
  94. new RegExp(arg.substr(1, arg.length - 2), 'gi'),
  95. bit => markStart + bit + markEnd
  96. )
  97. }
  98. // just a normal string, do the split/map thing
  99. var pieces = str.toLowerCase().split(arg.toLowerCase())
  100. var p = 0
  101. return pieces.map(function (piece) {
  102. piece = str.substr(p, piece.length)
  103. var mark = markStart +
  104. str.substr(p + piece.length, arg.length) +
  105. markEnd
  106. p += piece.length + arg.length
  107. return piece + mark
  108. }).join('')
  109. }
  110. function colorize (line) {
  111. for (var i = 0; i < cl; i++) {
  112. var m = i + 1
  113. var color = '\u001B[' + colors[i] + 'm'
  114. line = line.split(String.fromCharCode(m)).join(color)
  115. }
  116. var uncolor = '\u001B[0m'
  117. return line.split('\u0000').join(uncolor)
  118. }
  119. function getMaxWidth () {
  120. var cols
  121. try {
  122. var tty = require('tty')
  123. var stdout = process.stdout
  124. cols = !tty.isatty(stdout.fd) ? Infinity : process.stdout.getWindowSize()[0]
  125. cols = (cols === 0) ? Infinity : cols
  126. } catch (ex) {
  127. cols = Infinity
  128. }
  129. return cols
  130. }
  131. function trimToMaxWidth (str) {
  132. var maxWidth = getMaxWidth()
  133. return str.split('\n').map(function (line) {
  134. return line.slice(0, maxWidth)
  135. }).join('\n')
  136. }
  137. function highlightSearchTerms (str, terms) {
  138. terms.forEach(function (arg, i) {
  139. str = addColorMarker(str, arg, i)
  140. })
  141. return colorize(str).trim()
  142. }
  143. function normalizePackage (data, opts) {
  144. opts = opts || {}
  145. return {
  146. name: data.name,
  147. description: opts.description ? data.description : '',
  148. author: (data.maintainers || []).map(function (m) {
  149. return '=' + m.username
  150. }).join(' '),
  151. keywords: Array.isArray(data.keywords)
  152. ? data.keywords.join(' ')
  153. : typeof data.keywords === 'string'
  154. ? data.keywords.replace(/[,\s]+/, ' ')
  155. : '',
  156. version: data.version,
  157. date: (data.date &&
  158. (data.date.toISOString() // remove time
  159. .split('T').join(' ')
  160. .replace(/:[0-9]{2}\.[0-9]{3}Z$/, ''))
  161. .slice(0, -5)) ||
  162. 'prehistoric',
  163. }
  164. }