1 |
|
2 | module.exports = exports = search
|
3 |
|
4 | var npm = require("./npm.js")
|
5 | , registry = npm.registry
|
6 | , columnify = require('columnify')
|
7 | , fmt = require('printf')
|
8 | , cols = process.stdout.columns || 80
|
9 | , repo = require('github-url-from-git')
|
10 | , chalk = require('chalk')
|
11 | , mapToRegistry = require("./utils/map-to-registry.js")
|
12 |
|
13 | search.usage = "npm search [some search terms ...]"
|
14 |
|
15 | search.completion = function (opts, cb) {
|
16 | var compl = {}
|
17 | , partial = opts.partialWord
|
18 | , ipartial = partial.toLowerCase()
|
19 | , plen = partial.length
|
20 |
|
21 |
|
22 |
|
23 |
|
24 | search(opts.conf.argv.remain.slice(2), true, function (er, data) {
|
25 | if (er) return cb(er)
|
26 | Object.keys(data).forEach(function (name) {
|
27 | data[name].words.split(" ").forEach(function (w) {
|
28 | if (w.toLowerCase().indexOf(ipartial) === 0) {
|
29 | compl[partial + w.substr(plen)] = true
|
30 | }
|
31 | })
|
32 | })
|
33 | cb(null, Object.keys(compl))
|
34 | })
|
35 | }
|
36 |
|
37 | function search (args, silent, staleness, cb) {
|
38 | if (typeof cb !== "function") cb = staleness, staleness = 600
|
39 | if (typeof cb !== "function") cb = silent, silent = false
|
40 |
|
41 | var searchopts = npm.config.get("searchopts")
|
42 | , searchexclude = npm.config.get("searchexclude")
|
43 | if (typeof searchopts !== "string") searchopts = ""
|
44 | searchopts = searchopts.split(/\s+/)
|
45 | if (typeof searchexclude === "string") {
|
46 | searchexclude = searchexclude.split(/\s+/)
|
47 | } else searchexclude = []
|
48 | var opts = searchopts.concat(args).map(function (s) {
|
49 | return s.toLowerCase()
|
50 | }).filter(function (s) { return s })
|
51 | searchexclude = searchexclude.map(function (s) {
|
52 | return s.toLowerCase()
|
53 | })
|
54 | getFilteredData( staleness, opts, searchexclude, function (er, data) {
|
55 |
|
56 |
|
57 |
|
58 | if (er || silent) return cb(er, data)
|
59 | output(data, args)
|
60 | cb(null, data)
|
61 | })
|
62 | }
|
63 |
|
64 | function getFilteredData (staleness, args, notArgs, cb) {
|
65 | var opts = {
|
66 | timeout : staleness,
|
67 | follow : true,
|
68 | staleOk : true
|
69 | }
|
70 | mapToRegistry("-/all", npm.config, function (er, uri) {
|
71 | if (er) return cb(er)
|
72 |
|
73 | registry.get(uri, opts, function (er, data) {
|
74 | if (er) return cb(er)
|
75 | return cb(null, filter(data, args, notArgs))
|
76 | })
|
77 | })
|
78 | }
|
79 |
|
80 | function filter (data, args, notArgs) {
|
81 |
|
82 | return Object.keys(data).map(function (d) {
|
83 | return data[d]
|
84 | }).filter(function (d) {
|
85 | return typeof d === "object"
|
86 | }).map(stripData).map(getWords).filter(function (data) {
|
87 | return filterWords(data, args, notArgs)
|
88 | }).reduce(function (l, r) {
|
89 | l[r.name] = r
|
90 | return l
|
91 | }, {})
|
92 | }
|
93 |
|
94 | function stripData (data) {
|
95 | return { name: data.name
|
96 | , repository: data.repository
|
97 | , description: npm.config.get("description") ? data.description : ""
|
98 | , maintainers: (data.maintainers || []).map(function (m) {
|
99 | return "=" + m.name
|
100 | })
|
101 | , url: !Object.keys(data.versions || {}).length ? data.url : null
|
102 | , keywords: data.keywords || []
|
103 | , version: Object.keys(data.versions || {})[0] || []
|
104 | , time: data.time
|
105 | && data.time.modified
|
106 | && (new Date(data.time.modified).toISOString()
|
107 | .split("T").join(" ")
|
108 | .replace(/:[0-9]{2}\.[0-9]{3}Z$/, ""))
|
109 | .slice(0, -5)
|
110 | || "prehistoric"
|
111 | }
|
112 | }
|
113 |
|
114 | function getWords (data) {
|
115 | data.words = [ data.name ]
|
116 | .concat(data.description)
|
117 | .concat(data.maintainers)
|
118 | .concat(data.url && ("<" + data.url + ">"))
|
119 | .concat(data.keywords)
|
120 | .map(function (f) { return f && f.trim && f.trim() })
|
121 | .filter(function (f) { return f })
|
122 | .join(" ")
|
123 | .toLowerCase()
|
124 | return data
|
125 | }
|
126 |
|
127 | function filterWords (data, args, notArgs) {
|
128 | var words = data.words
|
129 | for (var i = 0, l = args.length; i < l; i ++) {
|
130 | if (!match(words, args[i])) return false
|
131 | }
|
132 | for (i = 0, l = notArgs.length; i < l; i ++) {
|
133 | if (match(words, notArgs[i])) return false
|
134 | }
|
135 | return true
|
136 | }
|
137 |
|
138 | function match (words, arg) {
|
139 | if (arg.charAt(0) === "/") {
|
140 | arg = arg.replace(/\/$/, "")
|
141 | arg = new RegExp(arg.substr(1, arg.length - 1))
|
142 | return words.match(arg)
|
143 | }
|
144 | return words.indexOf(arg) !== -1
|
145 | }
|
146 |
|
147 | function output (data, args) {
|
148 | var searchsort = (npm.config.get("searchsort") || "NAME").toLowerCase()
|
149 | , sortField = searchsort.replace(/^\-+/, "")
|
150 | , searchRev = searchsort.charAt(0) === "-"
|
151 | , truncate = !npm.config.get("long")
|
152 |
|
153 | if (Object.keys(data).length === 0) {
|
154 | return "No match found for "+(args.map(JSON.stringify).join(" "))
|
155 | }
|
156 |
|
157 | var lines = Object.keys(data).map(function (d) {
|
158 |
|
159 | return data[d]
|
160 | }).map(function(dat) {
|
161 | dat.author = dat.maintainers
|
162 | delete dat.maintainers
|
163 | dat.date = dat.time
|
164 | delete dat.time
|
165 | return dat
|
166 | }).map(function(dat) {
|
167 |
|
168 | if (typeof dat.keywords === "string") {
|
169 | dat.keywords = dat.keywords.split(/[,\s]+/)
|
170 | }
|
171 | if (Array.isArray(dat.keywords)) {
|
172 | dat.keywords = dat.keywords.join(' ')
|
173 | }
|
174 |
|
175 |
|
176 | if (typeof dat.author === "string") {
|
177 | dat.author = dat.author.split(/[,\s]+/)
|
178 | }
|
179 | if (Array.isArray(dat.author)) {
|
180 | dat.author = dat.author.join(' ')
|
181 | }
|
182 | return dat
|
183 | })
|
184 |
|
185 | lines.sort(function(a, b) {
|
186 | var aa = a[sortField].toLowerCase()
|
187 | , bb = b[sortField].toLowerCase()
|
188 | return aa === bb ? 0
|
189 | : aa < bb ? -1 : 1
|
190 | })
|
191 |
|
192 | if (searchRev) lines.reverse()
|
193 |
|
194 | var columns = npm.config.get("description")
|
195 | ? ["name", "description", "author", "date", "version", "keywords"]
|
196 | : ["name", "author", "date", "version", "keywords"]
|
197 |
|
198 | console.log();
|
199 | lines.forEach(function(pkg){
|
200 | args.forEach(function(arg){
|
201 | var desc = pkg.description || '';
|
202 | pkg.description = desc.replace(arg, function(arg){
|
203 | return chalk.underline(arg);
|
204 | });
|
205 | });
|
206 |
|
207 | console.log(' \033[36m%s\033[0m', pkg.name);
|
208 | if (pkg.repository) console.log(' repo: \033[90m%s\033[0m', repo(pkg.repository.url));
|
209 | console.log(' desc: \033[90m%s\033[0m', description(pkg.description));
|
210 | console.log(' by: \033[90m%s\033[0m', pkg.author.replace(/=/g, ''));
|
211 | console.log();
|
212 | });
|
213 | }
|
214 |
|
215 |
|
216 |
|
217 |
|
218 |
|
219 | function description(str) {
|
220 | if (!str) return '';
|
221 | var space;
|
222 | var width = cols - 20;
|
223 | for (var i = 0; i < str.length; ++i) {
|
224 | if (i && i % width == 0) {
|
225 | space = str.indexOf(' ', i);
|
226 | str = str.slice(0, space) + '\n ' + str.slice(space);
|
227 | }
|
228 | }
|
229 | return str;
|
230 | }
|
231 |
|
232 | var colors = [31, 33, 32, 34, 35 ]
|
233 | , cl = colors.length
|
234 |
|
235 | function addColorMarker (str, arg, i) {
|
236 | var m = i % cl + 1
|
237 | , markStart = String.fromCharCode(m)
|
238 | , markEnd = String.fromCharCode(0)
|
239 |
|
240 | if (arg.charAt(0) === "/") {
|
241 |
|
242 | return str.replace( new RegExp(arg.substr(1, arg.length - 2), "gi")
|
243 | , function (bit) { return markStart + bit + markEnd } )
|
244 |
|
245 | }
|
246 |
|
247 |
|
248 | var pieces = str.toLowerCase().split(arg.toLowerCase())
|
249 | , p = 0
|
250 |
|
251 | return pieces.map(function (piece) {
|
252 | piece = str.substr(p, piece.length)
|
253 | var mark = markStart
|
254 | + str.substr(p+piece.length, arg.length)
|
255 | + markEnd
|
256 | p += piece.length + arg.length
|
257 | return piece + mark
|
258 | }).join("")
|
259 | }
|
260 |
|
261 | function colorize (line) {
|
262 | for (var i = 0; i < cl; i ++) {
|
263 | var m = i + 1
|
264 | var color = npm.color ? "\033["+colors[i]+"m" : ""
|
265 | line = line.split(String.fromCharCode(m)).join(color)
|
266 | }
|
267 | var uncolor = npm.color ? "\033[0m" : ""
|
268 | return line.split("\u0000").join(uncolor)
|
269 | }
|
270 |
|
271 | function getMaxWidth() {
|
272 | var cols
|
273 | try {
|
274 | var tty = require("tty")
|
275 | , stdout = process.stdout
|
276 | cols = !tty.isatty(stdout.fd) ? Infinity : process.stdout.getWindowSize()[0]
|
277 | cols = (cols === 0) ? Infinity : cols
|
278 | } catch (ex) { cols = Infinity }
|
279 | return cols
|
280 | }
|
281 |
|
282 | function trimToMaxWidth(str) {
|
283 | var maxWidth = getMaxWidth()
|
284 | return str.split('\n').map(function(line) {
|
285 | return line.slice(0, maxWidth)
|
286 | }).join('\n')
|
287 | }
|
288 |
|
289 | function highlightSearchTerms(str, terms) {
|
290 | terms.forEach(function (arg, i) {
|
291 | str = addColorMarker(str, arg, i)
|
292 | })
|
293 |
|
294 | return colorize(str).trim()
|
295 | }
|