Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: refactor search formatting code #6995

Merged
merged 1 commit into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading