diff --git a/lib/utils/format-search-stream.js b/lib/utils/format-search-stream.js index b70bd915123da..9b144ceae1984 100644 --- a/lib/utils/format-search-stream.js +++ b/lib/utils/format-search-stream.js @@ -82,7 +82,8 @@ class TextOutputStream extends Minipass { constructor (opts) { super() - this.#args = opts.args.map(s => s.toLowerCase()).filter(Boolean) + // Consider a search for "cowboys" and "boy". If we highlight "boys" first the "cowboys" string will no longer string match because of the ansi highlighting added to "boys". If we highlight "boy" second then the ansi reset at the end will make the highlighting only on "cowboy" with a normal "s". Neither is perfect but at least the first option doesn't do partial highlighting. So, we sort strings smaller to larger + this.#args = opts.args.map(s => s.toLowerCase()).filter(Boolean).sort((a, b) => a.length - b.length) this.#chalk = opts.npm.chalk this.#exclude = opts.exclude this.#parseable = opts.parseable @@ -124,38 +125,17 @@ class TextOutputStream extends Minipass { } }).join(' ') - let description = [] - for (const arg of this.#args) { - const finder = pkg.description.toLowerCase().split(arg.toLowerCase()) - let p = 0 - for (const f of finder) { - description.push(pkg.description.slice(p, p + f.length)) - const word = pkg.description.slice(p + f.length, p + f.length + arg.length) - description.push(this.#chalk.cyan(word)) - p += f.length + arg.length - } - } - description = description.filter(Boolean) - let name = pkg.name + const description = this.#highlight(pkg.description) + let name if (this.#args.includes(pkg.name)) { name = this.#chalk.cyan(pkg.name) } else { - name = [] - for (const arg of this.#args) { - const finder = pkg.name.toLowerCase().split(arg.toLowerCase()) - let p = 0 - for (const f of finder) { - name.push(pkg.name.slice(p, p + f.length)) - const word = pkg.name.slice(p + f.length, p + f.length + arg.length) - name.push(this.#chalk.cyan(word)) - p += f.length + arg.length - } - } - name = this.#chalk.blue(name.join('')) + name = this.#highlight(pkg.name) + name = this.#chalk.blue(name) } if (description.length) { - output = `${name}\n${description.join('')}\n` + output = `${name}\n${description}\n` } else { output = `${name}\n` } @@ -171,4 +151,21 @@ class TextOutputStream extends Minipass { output += `${this.#chalk.blue(`https://npm.im/${pkg.name}`)}\n` return super.write(output) } + + #highlight (input) { + let output = input + for (const arg of this.#args) { + let i = output.toLowerCase().indexOf(arg) + while (i > -1) { + const highlit = this.#chalk.cyan(output.slice(i, i + arg.length)) + output = [ + output.slice(0, i), + highlit, + output.slice(i + arg.length), + ].join('') + i = output.toLowerCase().indexOf(arg, i + highlit.length) + } + } + return output + } } diff --git a/tap-snapshots/test/lib/commands/search.js.test.cjs b/tap-snapshots/test/lib/commands/search.js.test.cjs index f1fa0363c8e68..f8dc5d94f4b21 100644 --- a/tap-snapshots/test/lib/commands/search.js.test.cjs +++ b/tap-snapshots/test/lib/commands/search.js.test.cjs @@ -932,6 +932,152 @@ Maintainers: lukekarrys https://npm.im/pkg-no-desc ` +exports[`test/lib/commands/search.js TAP search multiple terms --color > should have expected search results with color 1`] = ` +libnpm +Collection of programmatic APIs for the npm CLI +Version 3.0.1 published 2019-07-16 by isaacs +Maintainers: nlf ruyadorno darcyclarke isaacs +Keywords: npm api package manager lib +https://npm.im/libnpm +libnpmaccess +programmatic library for \`npm access\` commands +Version 4.0.1 published 2020-11-03 by nlf +Maintainers: nlf ruyadorno darcyclarke isaacs +Keywords: libnpmaccess +https://npm.im/libnpmaccess +@evocateur/libnpmaccess +programmatic library for \`npm access\` commands +Version 3.1.2 published 2019-07-16 by evocateur +Maintainers: evocateur +https://npm.im/@evocateur/libnpmaccess +@evocateur/libnpmpublish +Programmatic API for the bits behind npm publish and unpublish +Version 1.2.2 published 2019-07-16 by evocateur +Maintainers: evocateur +https://npm.im/@evocateur/libnpmpublish +libnpmorg +Programmatic api for \`npm org\` commands +Version 2.0.1 published 2020-11-03 by nlf +Maintainers: nlf ruyadorno darcyclarke isaacs +Keywords: libnpm npm package manager api orgs teams +https://npm.im/libnpmorg +libnpmsearch +Programmatic API for searching in npm and compatible registries. +Version 3.1.0 published 2020-12-08 by isaacs +Maintainers: nlf ruyadorno darcyclarke isaacs +Keywords: npm search api libnpm +https://npm.im/libnpmsearch +libnpmteam +npm Team management APIs +Version 2.0.2 published 2020-11-03 by nlf +Maintainers: nlf ruyadorno darcyclarke isaacs +https://npm.im/libnpmteam +libnpmpublish +Programmatic API for the bits behind npm publish and unpublish +Version 4.0.0 published 2020-11-03 by nlf +Maintainers: nlf ruyadorno darcyclarke isaacs +https://npm.im/libnpmpublish +libnpmfund +Programmatic API for npm fund +Version 1.0.2 published 2020-12-08 by isaacs +Maintainers: nlf ruyadorno darcyclarke isaacs +Keywords: npm npmcli libnpm cli git fund gitfund +https://npm.im/libnpmfund +@npmcli/map-workspaces +Retrieves a name:pathname Map for a given workspaces config +Version 1.0.1 published 2020-09-30 by ruyadorno +Maintainers: nlf ruyadorno darcyclarke isaacs +Keywords: npm bad map npmcli libnpm cli workspaces map-workspaces +https://npm.im/@npmcli/map-workspaces +libnpmversion +library to do the things that 'npm version' does +Version 1.0.7 published 2020-11-04 by isaacs +Maintainers: nlf ruyadorno darcyclarke isaacs +https://npm.im/libnpmversion +@types/libnpmsearch +TypeScript definitions for libnpmsearch +Version 2.0.1 published 2019-09-26 by types +Maintainers: types +https://npm.im/@types/libnpmsearch +pkg-no-desc +Version 1.0.0 published 2019-09-26 by lukekarrys +Maintainers: lukekarrys +https://npm.im/pkg-no-desc +` + +exports[`test/lib/commands/search.js TAP search multiple terms text > should have expected search results 1`] = ` +libnpm +Collection of programmatic APIs for the npm CLI +Version 3.0.1 published 2019-07-16 by isaacs +Maintainers: nlf ruyadorno darcyclarke isaacs +Keywords: npm api package manager lib +https://npm.im/libnpm +libnpmaccess +programmatic library for \`npm access\` commands +Version 4.0.1 published 2020-11-03 by nlf +Maintainers: nlf ruyadorno darcyclarke isaacs +Keywords: libnpmaccess +https://npm.im/libnpmaccess +@evocateur/libnpmaccess +programmatic library for \`npm access\` commands +Version 3.1.2 published 2019-07-16 by evocateur +Maintainers: evocateur +https://npm.im/@evocateur/libnpmaccess +@evocateur/libnpmpublish +Programmatic API for the bits behind npm publish and unpublish +Version 1.2.2 published 2019-07-16 by evocateur +Maintainers: evocateur +https://npm.im/@evocateur/libnpmpublish +libnpmorg +Programmatic api for \`npm org\` commands +Version 2.0.1 published 2020-11-03 by nlf +Maintainers: nlf ruyadorno darcyclarke isaacs +Keywords: libnpm npm package manager api orgs teams +https://npm.im/libnpmorg +libnpmsearch +Programmatic API for searching in npm and compatible registries. +Version 3.1.0 published 2020-12-08 by isaacs +Maintainers: nlf ruyadorno darcyclarke isaacs +Keywords: npm search api libnpm +https://npm.im/libnpmsearch +libnpmteam +npm Team management APIs +Version 2.0.2 published 2020-11-03 by nlf +Maintainers: nlf ruyadorno darcyclarke isaacs +https://npm.im/libnpmteam +libnpmpublish +Programmatic API for the bits behind npm publish and unpublish +Version 4.0.0 published 2020-11-03 by nlf +Maintainers: nlf ruyadorno darcyclarke isaacs +https://npm.im/libnpmpublish +libnpmfund +Programmatic API for npm fund +Version 1.0.2 published 2020-12-08 by isaacs +Maintainers: nlf ruyadorno darcyclarke isaacs +Keywords: npm npmcli libnpm cli git fund gitfund +https://npm.im/libnpmfund +@npmcli/map-workspaces +Retrieves a name:pathname Map for a given workspaces config +Version 1.0.1 published 2020-09-30 by ruyadorno +Maintainers: nlf ruyadorno darcyclarke isaacs +Keywords: npm bad map npmcli libnpm cli workspaces map-workspaces +https://npm.im/@npmcli/map-workspaces +libnpmversion +library to do the things that 'npm version' does +Version 1.0.7 published 2020-11-04 by isaacs +Maintainers: nlf ruyadorno darcyclarke isaacs +https://npm.im/libnpmversion +@types/libnpmsearch +TypeScript definitions for libnpmsearch +Version 2.0.1 published 2019-09-26 by types +Maintainers: types +https://npm.im/@types/libnpmsearch +pkg-no-desc +Version 1.0.0 published 2019-09-26 by lukekarrys +Maintainers: lukekarrys +https://npm.im/pkg-no-desc +` + exports[`test/lib/commands/search.js TAP search no publisher > should have filtered expected search results 1`] = ` custom-registry Version 1.0.0 published prehistoric by ??? diff --git a/test/lib/commands/search.js b/test/lib/commands/search.js index de4a58ca78a8f..97adffd8e1432 100644 --- a/test/lib/commands/search.js +++ b/test/lib/commands/search.js @@ -26,6 +26,18 @@ t.test('search', t => { t.matchSnapshot(joinedOutput(), 'should have expected search results') }) + t.test('multiple terms text', async t => { + const { npm, joinedOutput } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + + registry.search({ results: libnpmsearchResultFixture }) + await npm.exec('search', ['libnpm', 'publish']) + t.matchSnapshot(joinedOutput(), 'should have expected search results') + }) + t.test(' --json', async t => { const { npm, joinedOutput } = await loadMockNpm(t, { config: { json: true } }) const registry = new MockRegistry({ @@ -68,6 +80,18 @@ t.test('search', t => { t.matchSnapshot(joinedOutput(), 'should have expected search results with color') }) + t.test('multiple terms --color', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { config: { color: 'always' } }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + + registry.search({ results: libnpmsearchResultFixture }) + await npm.exec('search', ['libnpm', 'publish']) + t.matchSnapshot(joinedOutput(), 'should have expected search results with color') + }) + t.test('//--color', async t => { const { npm, joinedOutput } = await loadMockNpm(t, { config: { color: 'always' } }) const registry = new MockRegistry({