Skip to content

Commit 021daf4

Browse files
committed
fix: refactor search formatting code (#6995)
output is the same but the code is more streamlined, and passes in the stripAnsi function as a "clean" function that can be extended or replaced later
1 parent 8d59d88 commit 021daf4

File tree

3 files changed

+101
-127
lines changed

3 files changed

+101
-127
lines changed

lib/commands/search.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,12 @@ class Search extends BaseCommand {
8181

8282
const filterStream = new FilterStream()
8383

84-
// Grab a configured output stream that will spit out packages in the
85-
// desired format.
86-
const outputStream = formatSearchStream({
84+
const { default: stripAnsi } = await import('strip-ansi')
85+
// Grab a configured output stream that will spit out packages in the desired format.
86+
const outputStream = await formatSearchStream({
8787
args, // --searchinclude options are not highlighted
8888
...opts,
89-
})
89+
}, stripAnsi)
9090

9191
log.silly('search', 'searching packages')
9292
const p = new Pipeline(

lib/utils/format-search-stream.js

+83-109
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
const { Minipass } = require('minipass')
22
const columnify = require('columnify')
3-
const ansiTrim = require('../utils/ansi-trim.js')
43

54
// This module consumes package data in the following format:
65
//
@@ -16,8 +15,8 @@ const ansiTrim = require('../utils/ansi-trim.js')
1615
// The returned stream will format this package data
1716
// into a byte stream of formatted, displayable output.
1817

19-
module.exports = (opts) => {
20-
return opts.json ? new JSONOutputStream() : new TextOutputStream(opts)
18+
module.exports = async (opts, clean) => {
19+
return opts.json ? new JSONOutputStream() : new TextOutputStream(opts, clean)
2120
}
2221

2322
class JSONOutputStream extends Minipass {
@@ -41,121 +40,96 @@ class JSONOutputStream extends Minipass {
4140
}
4241

4342
class TextOutputStream extends Minipass {
44-
constructor (opts) {
43+
#clean
44+
#opts
45+
#line = 0
46+
47+
constructor (opts, clean) {
4548
super()
46-
this._opts = opts
47-
this._line = 0
49+
this.#clean = clean
50+
this.#opts = opts
4851
}
4952

5053
write (pkg) {
51-
return super.write(prettify(pkg, ++this._line, this._opts))
52-
}
53-
}
54-
55-
function 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')
54+
return super.write(this.#prettify(pkg))
6655
}
6756

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-
},
57+
#prettify (data) {
58+
const pkg = {
59+
author: data.maintainers.map((m) => `=${this.#clean(m.username)}`).join(' '),
60+
date: 'prehistoric',
61+
description: this.#clean(data.description ?? ''),
62+
keywords: '',
63+
name: this.#clean(data.name),
64+
version: data.version,
65+
}
66+
if (Array.isArray(data.keywords)) {
67+
pkg.keywords = data.keywords.map((k) => this.#clean(k)).join(' ')
68+
} else if (typeof data.keywords === 'string') {
69+
pkg.keywords = this.#clean(data.keywords.replace(/[,\s]+/, ' '))
70+
}
71+
if (data.date) {
72+
pkg.date = data.date.toISOString().split('T')[0] // remove time
8673
}
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-
96-
var colors = [31, 33, 32, 36, 34, 35]
97-
var cl = colors.length
98-
99-
function 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-
125-
function 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-
}
13474

135-
function highlightSearchTerms (str, terms) {
136-
terms.forEach(function (arg, i) {
137-
str = addColorMarker(str, arg, i)
138-
})
75+
const columns = ['name', 'description', 'author', 'date', 'version', 'keywords']
76+
if (this.#opts.parseable) {
77+
return columns.map((col) => pkg[col] && ('' + pkg[col]).replace(/\t/g, ' ')).join('\t')
78+
}
13979

140-
return colorize(str).trim()
141-
}
80+
// stdout in tap is never a tty
81+
/* istanbul ignore next */
82+
const maxWidth = process.stdout.isTTY ? process.stdout.getWindowSize()[0] : Infinity
83+
let output = columnify(
84+
[pkg],
85+
{
86+
include: columns,
87+
showHeaders: ++this.#line <= 1,
88+
columnSplitter: ' | ',
89+
truncate: !this.#opts.long,
90+
config: {
91+
name: { minWidth: 25, maxWidth: 25, truncate: false, truncateMarker: '' },
92+
description: { minWidth: 20, maxWidth: 20 },
93+
author: { minWidth: 15, maxWidth: 15 },
94+
date: { maxWidth: 11 },
95+
version: { minWidth: 8, maxWidth: 8 },
96+
keywords: { maxWidth: Infinity },
97+
},
98+
}
99+
).split('\n').map(line => line.slice(0, maxWidth)).join('\n')
100+
101+
if (!this.#opts.color) {
102+
return output
103+
}
142104

143-
function 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',
105+
const colors = ['31m', '33m', '32m', '36m', '34m', '35m']
106+
107+
this.#opts.args.forEach((arg, i) => {
108+
const markStart = String.fromCharCode(i % colors.length + 1)
109+
const markEnd = String.fromCharCode(0)
110+
111+
if (arg.charAt(0) === '/') {
112+
output = output.replace(
113+
new RegExp(arg.slice(1, -1), 'gi'),
114+
bit => `${markStart}${bit}${markEnd}`
115+
)
116+
} else {
117+
// just a normal string, do the split/map thing
118+
let p = 0
119+
120+
output = output.toLowerCase().split(arg.toLowerCase()).map(piece => {
121+
piece = output.slice(p, p + piece.length)
122+
p += piece.length
123+
const mark = `${markStart}${output.slice(p, p + arg.length)}${markEnd}`
124+
p += arg.length
125+
return `${piece}${mark}`
126+
}).join('')
127+
}
128+
})
129+
130+
for (let i = 1; i <= colors.length; i++) {
131+
output = output.split(String.fromCharCode(i)).join(`\u001B[${colors[i - 1]}`)
132+
}
133+
return output.split('\u0000').join('\u001B[0m').trim()
160134
}
161135
}

tap-snapshots/test/lib/commands/search.js.test.cjs

+14-14
Original file line numberDiff line numberDiff line change
@@ -46,20 +46,20 @@ pkg-no-desc | | =lukekarrys | 2019-09-26
4646
`
4747

4848
exports[`test/lib/commands/search.js TAP search <name> --parseable > should have expected search results as parseable 1`] = `
49-
libnpm Collection of programmatic APIs for the npm CLI =nlf =ruyadorno =darcyclarke =isaacs 2019-07-16 3.0.1 npm api package manager lib
50-
libnpmaccess programmatic library for \`npm access\` commands =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 4.0.1 libnpmaccess
51-
@evocateur/libnpmaccess programmatic library for \`npm access\` commands =evocateur 2019-07-16 3.1.2
52-
@evocateur/libnpmpublish Programmatic API for the bits behind npm publish and unpublish =evocateur 2019-07-16 1.2.2
53-
libnpmorg Programmatic api for \`npm org\` commands =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 2.0.1 libnpm npm package manager api orgs teams
54-
libnpmsearch Programmatic API for searching in npm and compatible registries. =nlf =ruyadorno =darcyclarke =isaacs 2020-12-08 3.1.0 npm search api libnpm
55-
libnpmteam npm Team management APIs =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 2.0.2
56-
libnpmhook programmatic API for managing npm registry hooks =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 6.0.1 npm hooks registry npm api
57-
libnpmpublish Programmatic API for the bits behind npm publish and unpublish =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 4.0.0
58-
libnpmfund Programmatic API for npm fund =nlf =ruyadorno =darcyclarke =isaacs 2020-12-08 1.0.2 npm npmcli libnpm cli git fund gitfund
59-
@npmcli/map-workspaces Retrieves a name:pathname Map for a given workspaces config =nlf =ruyadorno =darcyclarke =isaacs 2020-09-30 1.0.1 npm npmcli libnpm cli workspaces map-workspaces
60-
libnpmversion library to do the things that 'npm version' does =nlf =ruyadorno =darcyclarke =isaacs 2020-11-04 1.0.7
61-
@types/libnpmsearch TypeScript definitions for libnpmsearch =types 2019-09-26 2.0.1
62-
pkg-no-desc =lukekarrys 2019-09-26 1.0.0
49+
libnpm Collection of programmatic APIs for the npm CLI =nlf =ruyadorno =darcyclarke =isaacs 2019-07-16 3.0.1 npm api package manager lib
50+
libnpmaccess programmatic library for \`npm access\` commands =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 4.0.1 libnpmaccess
51+
@evocateur/libnpmaccess programmatic library for \`npm access\` commands =evocateur 2019-07-16 3.1.2
52+
@evocateur/libnpmpublish Programmatic API for the bits behind npm publish and unpublish =evocateur 2019-07-16 1.2.2
53+
libnpmorg Programmatic api for \`npm org\` commands =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 2.0.1 libnpm npm package manager api orgs teams
54+
libnpmsearch Programmatic API for searching in npm and compatible registries. =nlf =ruyadorno =darcyclarke =isaacs 2020-12-08 3.1.0 npm search api libnpm
55+
libnpmteam npm Team management APIs =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 2.0.2
56+
libnpmhook programmatic API for managing npm registry hooks =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 6.0.1 npm hooks registry npm api
57+
libnpmpublish Programmatic API for the bits behind npm publish and unpublish =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 4.0.0
58+
libnpmfund Programmatic API for npm fund =nlf =ruyadorno =darcyclarke =isaacs 2020-12-08 1.0.2 npm npmcli libnpm cli git fund gitfund
59+
@npmcli/map-workspaces Retrieves a name:pathname Map for a given workspaces config =nlf =ruyadorno =darcyclarke =isaacs 2020-09-30 1.0.1 npm npmcli libnpm cli workspaces map-workspaces
60+
libnpmversion library to do the things that 'npm version' does =nlf =ruyadorno =darcyclarke =isaacs 2020-11-04 1.0.7
61+
@types/libnpmsearch TypeScript definitions for libnpmsearch =types 2019-09-26 2.0.1
62+
pkg-no-desc =lukekarrys 2019-09-26 1.0.0
6363
`
6464

6565
exports[`test/lib/commands/search.js TAP search <name> > should have filtered expected search results 1`] = `

0 commit comments

Comments
 (0)