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