• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1const { Minipass } = require('minipass')
2const columnify = require('columnify')
3const ansiTrim = require('../utils/ansi-trim.js')
4
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
19module.exports = (opts) => {
20  return opts.json ? new JSONOutputStream() : new TextOutputStream(opts)
21}
22
23class JSONOutputStream extends Minipass {
24  #didFirst = false
25
26  write (obj) {
27    if (!this.#didFirst) {
28      super.write('[\n')
29      this.#didFirst = true
30    } else {
31      super.write('\n,\n')
32    }
33
34    return super.write(JSON.stringify(obj))
35  }
36
37  end () {
38    super.write(this.#didFirst ? ']\n' : '\n[]\n')
39    super.end()
40  }
41}
42
43class TextOutputStream extends Minipass {
44  constructor (opts) {
45    super()
46    this._opts = opts
47    this._line = 0
48  }
49
50  write (pkg) {
51    return super.write(prettify(pkg, ++this._line, this._opts))
52  }
53}
54
55function prettify (data, num, opts) {
56  var truncate = !opts.long
57
58  var pkg = normalizePackage(data, opts)
59
60  var columns = ['name', 'description', 'author', 'date', 'version', 'keywords']
61
62  if (opts.parseable) {
63    return columns.map(function (col) {
64      return pkg[col] && ('' + pkg[col]).replace(/\t/g, ' ')
65    }).join('\t')
66  }
67
68  // stdout in tap is never a tty
69  /* istanbul ignore next */
70  const maxWidth = process.stdout.isTTY ? process.stdout.getWindowSize()[0] : Infinity
71  let output = columnify(
72    [pkg],
73    {
74      include: columns,
75      showHeaders: num <= 1,
76      columnSplitter: ' | ',
77      truncate: truncate,
78      config: {
79        name: { minWidth: 25, maxWidth: 25, truncate: false, truncateMarker: '' },
80        description: { minWidth: 20, maxWidth: 20 },
81        author: { minWidth: 15, maxWidth: 15 },
82        date: { maxWidth: 11 },
83        version: { minWidth: 8, maxWidth: 8 },
84        keywords: { maxWidth: Infinity },
85      },
86    }
87  ).split('\n').map(line => line.slice(0, maxWidth)).join('\n')
88
89  if (opts.color) {
90    output = highlightSearchTerms(output, opts.args)
91  }
92
93  return output
94}
95
96var colors = [31, 33, 32, 36, 34, 35]
97var cl = colors.length
98
99function addColorMarker (str, arg, i) {
100  var m = i % cl + 1
101  var markStart = String.fromCharCode(m)
102  var markEnd = String.fromCharCode(0)
103
104  if (arg.charAt(0) === '/') {
105    return str.replace(
106      new RegExp(arg.slice(1, -1), 'gi'),
107      bit => markStart + bit + markEnd
108    )
109  }
110
111  // just a normal string, do the split/map thing
112  var pieces = str.toLowerCase().split(arg.toLowerCase())
113  var p = 0
114
115  return pieces.map(function (piece) {
116    piece = str.slice(p, p + piece.length)
117    var mark = markStart +
118               str.slice(p + piece.length, p + piece.length + arg.length) +
119               markEnd
120    p += piece.length + arg.length
121    return piece + mark
122  }).join('')
123}
124
125function colorize (line) {
126  for (var i = 0; i < cl; i++) {
127    var m = i + 1
128    var color = '\u001B[' + colors[i] + 'm'
129    line = line.split(String.fromCharCode(m)).join(color)
130  }
131  var uncolor = '\u001B[0m'
132  return line.split('\u0000').join(uncolor)
133}
134
135function highlightSearchTerms (str, terms) {
136  terms.forEach(function (arg, i) {
137    str = addColorMarker(str, arg, i)
138  })
139
140  return colorize(str).trim()
141}
142
143function normalizePackage (data, opts) {
144  return {
145    name: ansiTrim(data.name),
146    description: ansiTrim(data.description),
147    author: data.maintainers.map((m) => `=${ansiTrim(m.username)}`).join(' '),
148    keywords: Array.isArray(data.keywords)
149      ? data.keywords.map(ansiTrim).join(' ')
150      : typeof data.keywords === 'string'
151        ? ansiTrim(data.keywords.replace(/[,\s]+/, ' '))
152        : '',
153    version: data.version,
154    date: (data.date &&
155          (data.date.toISOString() // remove time
156            .split('T').join(' ')
157            .replace(/:[0-9]{2}\.[0-9]{3}Z$/, ''))
158            .slice(0, -5)) ||
159          'prehistoric',
160  }
161}
162