• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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