UNPKG

5.78 kBJavaScriptView Raw
1
2module.exports = helpSearch
3
4var fs = require('graceful-fs')
5var path = require('path')
6var asyncMap = require('slide').asyncMap
7var npm = require('./npm.js')
8var glob = require('glob')
9var color = require('ansicolors')
10var output = require('./utils/output.js')
11
12helpSearch.usage = 'npm help-search <text>'
13
14function helpSearch (args, silent, cb) {
15 if (typeof cb !== 'function') {
16 cb = silent
17 silent = false
18 }
19 if (!args.length) return cb(helpSearch.usage)
20
21 var docPath = path.resolve(__dirname, '..', 'doc')
22 return glob(docPath + '/*/*.md', function (er, files) {
23 if (er) return cb(er)
24 readFiles(files, function (er, data) {
25 if (er) return cb(er)
26 searchFiles(args, data, function (er, results) {
27 if (er) return cb(er)
28 formatResults(args, results, cb)
29 })
30 })
31 })
32}
33
34function readFiles (files, cb) {
35 var res = {}
36 asyncMap(files, function (file, cb) {
37 fs.readFile(file, 'utf8', function (er, data) {
38 res[file] = data
39 return cb(er)
40 })
41 }, function (er) {
42 return cb(er, res)
43 })
44}
45
46function searchFiles (args, files, cb) {
47 var results = []
48 Object.keys(files).forEach(function (file) {
49 var data = files[file]
50
51 // skip if no matches at all
52 var match
53 for (var a = 0, l = args.length; a < l && !match; a++) {
54 match = data.toLowerCase().indexOf(args[a].toLowerCase()) !== -1
55 }
56 if (!match) return
57
58 var lines = data.split(/\n+/)
59
60 // if a line has a search term, then skip it and the next line.
61 // if the next line has a search term, then skip all 3
62 // otherwise, set the line to null. then remove the nulls.
63 l = lines.length
64 for (var i = 0; i < l; i++) {
65 var line = lines[i]
66 var nextLine = lines[i + 1]
67 var ll
68
69 match = false
70 if (nextLine) {
71 for (a = 0, ll = args.length; a < ll && !match; a++) {
72 match = nextLine.toLowerCase()
73 .indexOf(args[a].toLowerCase()) !== -1
74 }
75 if (match) {
76 // skip over the next line, and the line after it.
77 i += 2
78 continue
79 }
80 }
81
82 match = false
83 for (a = 0, ll = args.length; a < ll && !match; a++) {
84 match = line.toLowerCase().indexOf(args[a].toLowerCase()) !== -1
85 }
86 if (match) {
87 // skip over the next line
88 i++
89 continue
90 }
91
92 lines[i] = null
93 }
94
95 // now squish any string of nulls into a single null
96 lines = lines.reduce(function (l, r) {
97 if (!(r === null && l[l.length - 1] === null)) l.push(r)
98 return l
99 }, [])
100
101 if (lines[lines.length - 1] === null) lines.pop()
102 if (lines[0] === null) lines.shift()
103
104 // now see how many args were found at all.
105 var found = {}
106 var totalHits = 0
107 lines.forEach(function (line) {
108 args.forEach(function (arg) {
109 var hit = (line || '').toLowerCase()
110 .split(arg.toLowerCase()).length - 1
111 if (hit > 0) {
112 found[arg] = (found[arg] || 0) + hit
113 totalHits += hit
114 }
115 })
116 })
117
118 var cmd = 'npm help '
119 if (path.basename(path.dirname(file)) === 'api') {
120 cmd = 'npm apihelp '
121 }
122 cmd += path.basename(file, '.md').replace(/^npm-/, '')
123 results.push({
124 file: file,
125 cmd: cmd,
126 lines: lines,
127 found: Object.keys(found),
128 hits: found,
129 totalHits: totalHits
130 })
131 })
132
133 // if only one result, then just show that help section.
134 if (results.length === 1) {
135 return npm.commands.help([results[0].file.replace(/\.md$/, '')], cb)
136 }
137
138 if (results.length === 0) {
139 output('No results for ' + args.map(JSON.stringify).join(' '))
140 return cb()
141 }
142
143 // sort results by number of results found, then by number of hits
144 // then by number of matching lines
145 results = results.sort(function (a, b) {
146 return a.found.length > b.found.length ? -1
147 : a.found.length < b.found.length ? 1
148 : a.totalHits > b.totalHits ? -1
149 : a.totalHits < b.totalHits ? 1
150 : a.lines.length > b.lines.length ? -1
151 : a.lines.length < b.lines.length ? 1
152 : 0
153 })
154
155 cb(null, results)
156}
157
158function formatResults (args, results, cb) {
159 if (!results) return cb(null)
160
161 var cols = Math.min(process.stdout.columns || Infinity, 80) + 1
162
163 var out = results.map(function (res) {
164 var out = res.cmd
165 var r = Object.keys(res.hits)
166 .map(function (k) {
167 return k + ':' + res.hits[k]
168 }).sort(function (a, b) {
169 return a > b ? 1 : -1
170 }).join(' ')
171
172 out += ((new Array(Math.max(1, cols - out.length - r.length)))
173 .join(' ')) + r
174
175 if (!npm.config.get('long')) return out
176
177 out = '\n\n' + out + '\n' +
178 (new Array(cols)).join('—') + '\n' +
179 res.lines.map(function (line, i) {
180 if (line === null || i > 3) return ''
181 for (var out = line, a = 0, l = args.length; a < l; a++) {
182 var finder = out.toLowerCase().split(args[a].toLowerCase())
183 var newOut = ''
184 var p = 0
185
186 finder.forEach(function (f) {
187 newOut += out.substr(p, f.length)
188
189 var hilit = out.substr(p + f.length, args[a].length)
190 if (npm.color) hilit = color.bgBlack(color.red(hilit))
191 newOut += hilit
192
193 p += f.length + args[a].length
194 })
195 }
196
197 return newOut
198 }).join('\n').trim()
199 return out
200 }).join('\n')
201
202 if (results.length && !npm.config.get('long')) {
203 out = 'Top hits for ' + (args.map(JSON.stringify).join(' ')) + '\n' +
204 (new Array(cols)).join('—') + '\n' +
205 out + '\n' +
206 (new Array(cols)).join('—') + '\n' +
207 '(run with -l or --long to see more context)'
208 }
209
210 output(out.trim())
211 cb(null, results)
212}