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} 213