Skip to content

Commit

Permalink
fix: refactor search formatting code (#6995)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
wraithgar authored Nov 14, 2023
1 parent e9ec2f7 commit cd291e7
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 128 deletions.
6 changes: 3 additions & 3 deletions lib/commands/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,12 @@ class Search extends BaseCommand {

const filterStream = new FilterStream()

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

log.silly('search', 'searching packages')
const p = new Pipeline(
Expand Down
194 changes: 83 additions & 111 deletions lib/utils/format-search-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,8 @@ const columnify = require('columnify')
// The returned stream will format this package data
// into a byte stream of formatted, displayable output.

let stripAnsi
module.exports = async (opts) => {
stripAnsi = await import('strip-ansi')
stripAnsi = stripAnsi.default
return opts.json ? new JSONOutputStream() : new TextOutputStream(opts)
module.exports = async (opts, clean) => {
return opts.json ? new JSONOutputStream() : new TextOutputStream(opts, clean)
}

class JSONOutputStream extends Minipass {
Expand All @@ -43,121 +40,96 @@ class JSONOutputStream extends Minipass {
}

class TextOutputStream extends Minipass {
constructor (opts) {
#clean
#opts
#line = 0

constructor (opts, clean) {
super()
this._opts = opts
this._line = 0
this.#clean = clean
this.#opts = opts
}

write (pkg) {
return super.write(prettify(pkg, ++this._line, this._opts))
}
}

function prettify (data, num, opts) {
var truncate = !opts.long

var pkg = normalizePackage(data, opts)

var columns = ['name', 'description', 'author', 'date', 'version', 'keywords']

if (opts.parseable) {
return columns.map(function (col) {
return pkg[col] && ('' + pkg[col]).replace(/\t/g, ' ')
}).join('\t')
return super.write(this.#prettify(pkg))
}

// stdout in tap is never a tty
/* istanbul ignore next */
const maxWidth = process.stdout.isTTY ? process.stdout.getWindowSize()[0] : Infinity
let output = columnify(
[pkg],
{
include: columns,
showHeaders: num <= 1,
columnSplitter: ' | ',
truncate: truncate,
config: {
name: { minWidth: 25, maxWidth: 25, truncate: false, truncateMarker: '' },
description: { minWidth: 20, maxWidth: 20 },
author: { minWidth: 15, maxWidth: 15 },
date: { maxWidth: 11 },
version: { minWidth: 8, maxWidth: 8 },
keywords: { maxWidth: Infinity },
},
#prettify (data) {
const pkg = {
author: data.maintainers.map((m) => `=${this.#clean(m.username)}`).join(' '),
date: 'prehistoric',
description: this.#clean(data.description ?? ''),
keywords: '',
name: this.#clean(data.name),
version: data.version,
}
if (Array.isArray(data.keywords)) {
pkg.keywords = data.keywords.map((k) => this.#clean(k)).join(' ')
} else if (typeof data.keywords === 'string') {
pkg.keywords = this.#clean(data.keywords.replace(/[,\s]+/, ' '))
}
if (data.date) {
pkg.date = data.date.toISOString().split('T')[0] // remove time
}
).split('\n').map(line => line.slice(0, maxWidth)).join('\n')

if (opts.color) {
output = highlightSearchTerms(output, opts.args)
}

return output
}

var colors = [31, 33, 32, 36, 34, 35]
var cl = colors.length

function addColorMarker (str, arg, i) {
var m = i % cl + 1
var markStart = String.fromCharCode(m)
var markEnd = String.fromCharCode(0)

if (arg.charAt(0) === '/') {
return str.replace(
new RegExp(arg.slice(1, -1), 'gi'),
bit => markStart + bit + markEnd
)
}

// just a normal string, do the split/map thing
var pieces = str.toLowerCase().split(arg.toLowerCase())
var p = 0

return pieces.map(function (piece) {
piece = str.slice(p, p + piece.length)
var mark = markStart +
str.slice(p + piece.length, p + piece.length + arg.length) +
markEnd
p += piece.length + arg.length
return piece + mark
}).join('')
}

function colorize (line) {
for (var i = 0; i < cl; i++) {
var m = i + 1
var color = '\u001B[' + colors[i] + 'm'
line = line.split(String.fromCharCode(m)).join(color)
}
var uncolor = '\u001B[0m'
return line.split('\u0000').join(uncolor)
}

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

return colorize(str).trim()
}
// stdout in tap is never a tty
/* istanbul ignore next */
const maxWidth = process.stdout.isTTY ? process.stdout.getWindowSize()[0] : Infinity
let output = columnify(
[pkg],
{
include: columns,
showHeaders: ++this.#line <= 1,
columnSplitter: ' | ',
truncate: !this.#opts.long,
config: {
name: { minWidth: 25, maxWidth: 25, truncate: false, truncateMarker: '' },
description: { minWidth: 20, maxWidth: 20 },
author: { minWidth: 15, maxWidth: 15 },
date: { maxWidth: 11 },
version: { minWidth: 8, maxWidth: 8 },
keywords: { maxWidth: Infinity },
},
}
).split('\n').map(line => line.slice(0, maxWidth)).join('\n')

if (!this.#opts.color) {
return output
}

function normalizePackage (data, opts) {
return {
name: stripAnsi(data.name),
description: stripAnsi(data.description ?? ''),
author: data.maintainers.map((m) => `=${stripAnsi(m.username)}`).join(' '),
keywords: Array.isArray(data.keywords)
? data.keywords.map(stripAnsi).join(' ')
: typeof data.keywords === 'string'
? stripAnsi(data.keywords.replace(/[,\s]+/, ' '))
: '',
version: data.version,
date: (data.date &&
(data.date.toISOString() // remove time
.split('T').join(' ')
.replace(/:[0-9]{2}\.[0-9]{3}Z$/, ''))
.slice(0, -5)) ||
'prehistoric',
const colors = ['31m', '33m', '32m', '36m', '34m', '35m']

this.#opts.args.forEach((arg, i) => {
const markStart = String.fromCharCode(i % colors.length + 1)
const markEnd = String.fromCharCode(0)

if (arg.charAt(0) === '/') {
output = output.replace(
new RegExp(arg.slice(1, -1), 'gi'),
bit => `${markStart}${bit}${markEnd}`
)
} else {
// just a normal string, do the split/map thing
let p = 0

output = output.toLowerCase().split(arg.toLowerCase()).map(piece => {
piece = output.slice(p, p + piece.length)
p += piece.length
const mark = `${markStart}${output.slice(p, p + arg.length)}${markEnd}`
p += arg.length
return `${piece}${mark}`
}).join('')
}
})

for (let i = 1; i <= colors.length; i++) {
output = output.split(String.fromCharCode(i)).join(`\u001B[${colors[i - 1]}`)
}
return output.split('\u0000').join('\u001B[0m').trim()
}
}
28 changes: 14 additions & 14 deletions tap-snapshots/test/lib/commands/search.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,20 @@ pkg-no-desc | | =lukekarrys | 2019-09-26
`

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

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

0 comments on commit cd291e7

Please sign in to comment.