help-search.js 5.6 KB

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