1const { stripVTControlCharacters } = require('node:util')
2const { Minipass } = require('minipass')
3const columnify = require('columnify')
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 = async (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  #opts
45  #line = 0
46
47  constructor (opts) {
48    super()
49    this.#opts = opts
50  }
51
52  write (pkg) {
53    return super.write(this.#prettify(pkg))
54  }
55
56  #prettify (data) {
57    const pkg = {
58      author: data.maintainers.map((m) => `=${stripVTControlCharacters(m.username)}`).join(' '),
59      date: 'prehistoric',
60      description: stripVTControlCharacters(data.description ?? ''),
61      keywords: '',
62      name: stripVTControlCharacters(data.name),
63      version: data.version,
64    }
65    if (Array.isArray(data.keywords)) {
66      pkg.keywords = data.keywords.map((k) => stripVTControlCharacters(k)).join(' ')
67    } else if (typeof data.keywords === 'string') {
68      pkg.keywords = stripVTControlCharacters(data.keywords.replace(/[,\s]+/, ' '))
69    }
70    if (data.date) {
71      pkg.date = data.date.toISOString().split('T')[0] // remove time
72    }
73
74    const columns = ['name', 'description', 'author', 'date', 'version', 'keywords']
75    if (this.#opts.parseable) {
76      return columns.map((col) => pkg[col] && ('' + pkg[col]).replace(/\t/g, ' ')).join('\t')
77    }
78
79    // stdout in tap is never a tty
80    /* istanbul ignore next */
81    const maxWidth = process.stdout.isTTY ? process.stdout.getWindowSize()[0] : Infinity
82    let output = columnify(
83      [pkg],
84      {
85        include: columns,
86        showHeaders: ++this.#line <= 1,
87        columnSplitter: ' | ',
88        truncate: !this.#opts.long,
89        config: {
90          name: { minWidth: 25, maxWidth: 25, truncate: false, truncateMarker: '' },
91          description: { minWidth: 20, maxWidth: 20 },
92          author: { minWidth: 15, maxWidth: 15 },
93          date: { maxWidth: 11 },
94          version: { minWidth: 8, maxWidth: 8 },
95          keywords: { maxWidth: Infinity },
96        },
97      }
98    ).split('\n').map(line => line.slice(0, maxWidth)).join('\n')
99
100    if (!this.#opts.color) {
101      return output
102    }
103
104    const colors = ['31m', '33m', '32m', '36m', '34m', '35m']
105
106    this.#opts.args.forEach((arg, i) => {
107      const markStart = String.fromCharCode(i % colors.length + 1)
108      const markEnd = String.fromCharCode(0)
109
110      if (arg.charAt(0) === '/') {
111        output = output.replace(
112          new RegExp(arg.slice(1, -1), 'gi'),
113          bit => `${markStart}${bit}${markEnd}`
114        )
115      } else {
116        // just a normal string, do the split/map thing
117        let p = 0
118
119        output = output.toLowerCase().split(arg.toLowerCase()).map(piece => {
120          piece = output.slice(p, p + piece.length)
121          p += piece.length
122          const mark = `${markStart}${output.slice(p, p + arg.length)}${markEnd}`
123          p += arg.length
124          return `${piece}${mark}`
125        }).join('')
126      }
127    })
128
129    for (let i = 1; i <= colors.length; i++) {
130      output = output.split(String.fromCharCode(i)).join(`\u001B[${colors[i - 1]}`)
131    }
132    return output.split('\u0000').join('\u001B[0m').trim()
133  }
134}
135