2 module.exports = exports = search
4 var npm = require("./npm.js")
5 , columnify = require("columnify")
6 , updateIndex = require("./cache/update-index.js")
8 search.usage = "npm search [some search terms ...]"
10 search.completion = function (opts, cb) {
12 , partial = opts.partialWord
13 , ipartial = partial.toLowerCase()
14 , plen = partial.length
16 // get the batch of data that matches so far.
17 // this is an example of using npm.commands.search programmatically
18 // to fetch data that has been filtered by a set of arguments.
19 search(opts.conf.argv.remain.slice(2), true, function (er, data) {
21 Object.keys(data).forEach(function (name) {
22 data[name].words.split(" ").forEach(function (w) {
23 if (w.toLowerCase().indexOf(ipartial) === 0) {
24 compl[partial + w.substr(plen)] = true
28 cb(null, Object.keys(compl))
32 function search (args, silent, staleness, cb) {
33 if (typeof cb !== "function") cb = staleness, staleness = 600
34 if (typeof cb !== "function") cb = silent, silent = false
36 var searchopts = npm.config.get("searchopts")
37 var searchexclude = npm.config.get("searchexclude")
39 if (typeof searchopts !== "string") searchopts = ""
40 searchopts = searchopts.split(/\s+/)
41 var opts = searchopts.concat(args).map(function (s) {
42 return s.toLowerCase()
43 }).filter(function (s) { return s })
45 if (typeof searchexclude === "string") {
46 searchexclude = searchexclude.split(/\s+/)
50 searchexclude = searchexclude.map(function (s) {
51 return s.toLowerCase()
54 getFilteredData(staleness, opts, searchexclude, function (er, data) {
55 // now data is the list of data that we want to show.
56 // prettify and print it, and then provide the raw
58 if (er || silent) return cb(er, data)
59 console.log(prettify(data, args))
64 function getFilteredData (staleness, args, notArgs, cb) {
65 updateIndex(staleness, function (er, data) {
67 return cb(null, filter(data, args, notArgs))
71 function filter (data, args, notArgs) {
72 // data={<name>:{package data}}
73 return Object.keys(data).map(function (d) {
75 }).filter(function (d) {
76 return typeof d === "object"
77 }).map(stripData).map(getWords).filter(function (data) {
78 return filterWords(data, args, notArgs)
79 }).reduce(function (l, r) {
85 function stripData (data) {
86 return { name: data.name
87 , description: npm.config.get("description") ? data.description : ""
88 , maintainers: (data.maintainers || []).map(function (m) {
91 , url: !Object.keys(data.versions || {}).length ? data.url : null
92 , keywords: data.keywords || []
93 , version: Object.keys(data.versions || {})[0] || []
96 && (new Date(data.time.modified).toISOString()
98 .replace(/:[0-9]{2}\.[0-9]{3}Z$/, ""))
99 .slice(0, -5) // remove time
104 function getWords (data) {
105 data.words = [ data.name ]
106 .concat(data.description)
107 .concat(data.maintainers)
108 .concat(data.url && ("<" + data.url + ">"))
109 .concat(data.keywords)
110 .map(function (f) { return f && f.trim && f.trim() })
111 .filter(function (f) { return f })
117 function filterWords (data, args, notArgs) {
118 var words = data.words
119 for (var i = 0, l = args.length; i < l; i ++) {
120 if (!match(words, args[i])) return false
122 for (i = 0, l = notArgs.length; i < l; i ++) {
123 if (match(words, notArgs[i])) return false
128 function match (words, arg) {
129 if (arg.charAt(0) === "/") {
130 arg = arg.replace(/\/$/, "")
131 arg = new RegExp(arg.substr(1, arg.length - 1))
132 return words.match(arg)
134 return words.indexOf(arg) !== -1
137 function prettify (data, args) {
138 var searchsort = (npm.config.get("searchsort") || "NAME").toLowerCase()
139 , sortField = searchsort.replace(/^\-+/, "")
140 , searchRev = searchsort.charAt(0) === "-"
141 , truncate = !npm.config.get("long")
143 if (Object.keys(data).length === 0) {
144 return "No match found for "+(args.map(JSON.stringify).join(" "))
147 var lines = Object.keys(data).map(function (d) {
150 }).map(function(dat) {
151 dat.author = dat.maintainers
152 delete dat.maintainers
156 }).map(function(dat) {
157 // split keywords on whitespace or ,
158 if (typeof dat.keywords === "string") {
159 dat.keywords = dat.keywords.split(/[,\s]+/)
161 if (Array.isArray(dat.keywords)) {
162 dat.keywords = dat.keywords.join(" ")
165 // split author on whitespace or ,
166 if (typeof dat.author === "string") {
167 dat.author = dat.author.split(/[,\s]+/)
169 if (Array.isArray(dat.author)) {
170 dat.author = dat.author.join(" ")
175 lines.sort(function(a, b) {
176 var aa = a[sortField].toLowerCase()
177 , bb = b[sortField].toLowerCase()
182 if (searchRev) lines.reverse()
184 var columns = npm.config.get("description")
185 ? ["name", "description", "author", "date", "version", "keywords"]
186 : ["name", "author", "date", "version", "keywords"]
188 var output = columnify(lines, {
192 name: { maxWidth: 40, truncate: false, truncateMarker: "" }
193 , description: { maxWidth: 60 }
194 , author: { maxWidth: 20 }
195 , date: { maxWidth: 11 }
196 , version: { maxWidth: 11 }
197 , keywords: { maxWidth: Infinity }
200 output = trimToMaxWidth(output)
201 output = highlightSearchTerms(output, args)
206 var colors = [31, 33, 32, 36, 34, 35 ]
209 function addColorMarker (str, arg, i) {
211 , markStart = String.fromCharCode(m)
212 , markEnd = String.fromCharCode(0)
214 if (arg.charAt(0) === "/") {
215 //arg = arg.replace(/\/$/, "")
216 return str.replace( new RegExp(arg.substr(1, arg.length - 2), "gi")
217 , function (bit) { return markStart + bit + markEnd } )
221 // just a normal string, do the split/map thing
222 var pieces = str.toLowerCase().split(arg.toLowerCase())
225 return pieces.map(function (piece) {
226 piece = str.substr(p, piece.length)
228 + str.substr(p+piece.length, arg.length)
230 p += piece.length + arg.length
235 function colorize (line) {
236 for (var i = 0; i < cl; i ++) {
238 var color = npm.color ? "\033["+colors[i]+"m" : ""
239 line = line.split(String.fromCharCode(m)).join(color)
241 var uncolor = npm.color ? "\033[0m" : ""
242 return line.split("\u0000").join(uncolor)
245 function getMaxWidth() {
248 var tty = require("tty")
249 , stdout = process.stdout
250 cols = !tty.isatty(stdout.fd) ? Infinity : process.stdout.getWindowSize()[0]
251 cols = (cols === 0) ? Infinity : cols
252 } catch (ex) { cols = Infinity }
256 function trimToMaxWidth(str) {
257 var maxWidth = getMaxWidth()
258 return str.split("\n").map(function(line) {
259 return line.slice(0, maxWidth)
263 function highlightSearchTerms(str, terms) {
264 terms.forEach(function (arg, i) {
265 str = addColorMarker(str, arg, i)
268 return colorize(str).trim()