11cb0ef41Sopenharmony_ciconst { readFile } = require('fs/promises')
21cb0ef41Sopenharmony_ciconst path = require('path')
31cb0ef41Sopenharmony_ciconst { glob } = require('glob')
41cb0ef41Sopenharmony_ciconst BaseCommand = require('../base-command.js')
51cb0ef41Sopenharmony_ci
61cb0ef41Sopenharmony_ciconst globify = pattern => pattern.split('\\').join('/')
71cb0ef41Sopenharmony_ci
81cb0ef41Sopenharmony_ciclass HelpSearch extends BaseCommand {
91cb0ef41Sopenharmony_ci  static description = 'Search npm help documentation'
101cb0ef41Sopenharmony_ci  static name = 'help-search'
111cb0ef41Sopenharmony_ci  static usage = ['<text>']
121cb0ef41Sopenharmony_ci  static params = ['long']
131cb0ef41Sopenharmony_ci
141cb0ef41Sopenharmony_ci  async exec (args) {
151cb0ef41Sopenharmony_ci    if (!args.length) {
161cb0ef41Sopenharmony_ci      throw this.usageError()
171cb0ef41Sopenharmony_ci    }
181cb0ef41Sopenharmony_ci
191cb0ef41Sopenharmony_ci    const docPath = path.resolve(this.npm.npmRoot, 'docs/content')
201cb0ef41Sopenharmony_ci    let files = await glob(`${globify(docPath)}/*/*.md`)
211cb0ef41Sopenharmony_ci    // preserve glob@8 behavior
221cb0ef41Sopenharmony_ci    files = files.sort((a, b) => a.localeCompare(b, 'en'))
231cb0ef41Sopenharmony_ci    const data = await this.readFiles(files)
241cb0ef41Sopenharmony_ci    const results = await this.searchFiles(args, data, files)
251cb0ef41Sopenharmony_ci    const formatted = this.formatResults(args, results)
261cb0ef41Sopenharmony_ci    if (!formatted.trim()) {
271cb0ef41Sopenharmony_ci      this.npm.output(`No matches in help for: ${args.join(' ')}\n`)
281cb0ef41Sopenharmony_ci    } else {
291cb0ef41Sopenharmony_ci      this.npm.output(formatted)
301cb0ef41Sopenharmony_ci    }
311cb0ef41Sopenharmony_ci  }
321cb0ef41Sopenharmony_ci
331cb0ef41Sopenharmony_ci  async readFiles (files) {
341cb0ef41Sopenharmony_ci    const res = {}
351cb0ef41Sopenharmony_ci    await Promise.all(files.map(async file => {
361cb0ef41Sopenharmony_ci      res[file] = (await readFile(file, 'utf8'))
371cb0ef41Sopenharmony_ci        .replace(/^---\n(.*\n)*?---\n/, '').trim()
381cb0ef41Sopenharmony_ci    }))
391cb0ef41Sopenharmony_ci    return res
401cb0ef41Sopenharmony_ci  }
411cb0ef41Sopenharmony_ci
421cb0ef41Sopenharmony_ci  async searchFiles (args, data, files) {
431cb0ef41Sopenharmony_ci    const results = []
441cb0ef41Sopenharmony_ci    for (const [file, content] of Object.entries(data)) {
451cb0ef41Sopenharmony_ci      const lowerCase = content.toLowerCase()
461cb0ef41Sopenharmony_ci      // skip if no matches at all
471cb0ef41Sopenharmony_ci      if (!args.some(a => lowerCase.includes(a.toLowerCase()))) {
481cb0ef41Sopenharmony_ci        continue
491cb0ef41Sopenharmony_ci      }
501cb0ef41Sopenharmony_ci
511cb0ef41Sopenharmony_ci      const lines = content.split(/\n+/)
521cb0ef41Sopenharmony_ci
531cb0ef41Sopenharmony_ci      // if a line has a search term, then skip it and the next line.
541cb0ef41Sopenharmony_ci      // if the next line has a search term, then skip all 3
551cb0ef41Sopenharmony_ci      // otherwise, set the line to null.  then remove the nulls.
561cb0ef41Sopenharmony_ci      for (let i = 0; i < lines.length; i++) {
571cb0ef41Sopenharmony_ci        const line = lines[i]
581cb0ef41Sopenharmony_ci        const nextLine = lines[i + 1]
591cb0ef41Sopenharmony_ci        let match = false
601cb0ef41Sopenharmony_ci        if (nextLine) {
611cb0ef41Sopenharmony_ci          match = args.some(a =>
621cb0ef41Sopenharmony_ci            nextLine.toLowerCase().includes(a.toLowerCase()))
631cb0ef41Sopenharmony_ci          if (match) {
641cb0ef41Sopenharmony_ci            // skip over the next line, and the line after it.
651cb0ef41Sopenharmony_ci            i += 2
661cb0ef41Sopenharmony_ci            continue
671cb0ef41Sopenharmony_ci          }
681cb0ef41Sopenharmony_ci        }
691cb0ef41Sopenharmony_ci
701cb0ef41Sopenharmony_ci        match = args.some(a => line.toLowerCase().includes(a.toLowerCase()))
711cb0ef41Sopenharmony_ci
721cb0ef41Sopenharmony_ci        if (match) {
731cb0ef41Sopenharmony_ci          // skip over the next line
741cb0ef41Sopenharmony_ci          i++
751cb0ef41Sopenharmony_ci          continue
761cb0ef41Sopenharmony_ci        }
771cb0ef41Sopenharmony_ci
781cb0ef41Sopenharmony_ci        lines[i] = null
791cb0ef41Sopenharmony_ci      }
801cb0ef41Sopenharmony_ci
811cb0ef41Sopenharmony_ci      // now squish any string of nulls into a single null
821cb0ef41Sopenharmony_ci      const pruned = lines.reduce((l, r) => {
831cb0ef41Sopenharmony_ci        if (!(r === null && l[l.length - 1] === null)) {
841cb0ef41Sopenharmony_ci          l.push(r)
851cb0ef41Sopenharmony_ci        }
861cb0ef41Sopenharmony_ci
871cb0ef41Sopenharmony_ci        return l
881cb0ef41Sopenharmony_ci      }, [])
891cb0ef41Sopenharmony_ci
901cb0ef41Sopenharmony_ci      if (pruned[pruned.length - 1] === null) {
911cb0ef41Sopenharmony_ci        pruned.pop()
921cb0ef41Sopenharmony_ci      }
931cb0ef41Sopenharmony_ci
941cb0ef41Sopenharmony_ci      if (pruned[0] === null) {
951cb0ef41Sopenharmony_ci        pruned.shift()
961cb0ef41Sopenharmony_ci      }
971cb0ef41Sopenharmony_ci
981cb0ef41Sopenharmony_ci      // now count how many args were found
991cb0ef41Sopenharmony_ci      const found = {}
1001cb0ef41Sopenharmony_ci      let totalHits = 0
1011cb0ef41Sopenharmony_ci      for (const line of pruned) {
1021cb0ef41Sopenharmony_ci        for (const arg of args) {
1031cb0ef41Sopenharmony_ci          const hit = (line || '').toLowerCase()
1041cb0ef41Sopenharmony_ci            .split(arg.toLowerCase()).length - 1
1051cb0ef41Sopenharmony_ci
1061cb0ef41Sopenharmony_ci          if (hit > 0) {
1071cb0ef41Sopenharmony_ci            found[arg] = (found[arg] || 0) + hit
1081cb0ef41Sopenharmony_ci            totalHits += hit
1091cb0ef41Sopenharmony_ci          }
1101cb0ef41Sopenharmony_ci        }
1111cb0ef41Sopenharmony_ci      }
1121cb0ef41Sopenharmony_ci
1131cb0ef41Sopenharmony_ci      const cmd = 'npm help ' +
1141cb0ef41Sopenharmony_ci        path.basename(file, '.md').replace(/^npm-/, '')
1151cb0ef41Sopenharmony_ci      results.push({
1161cb0ef41Sopenharmony_ci        file,
1171cb0ef41Sopenharmony_ci        cmd,
1181cb0ef41Sopenharmony_ci        lines: pruned,
1191cb0ef41Sopenharmony_ci        found: Object.keys(found),
1201cb0ef41Sopenharmony_ci        hits: found,
1211cb0ef41Sopenharmony_ci        totalHits,
1221cb0ef41Sopenharmony_ci      })
1231cb0ef41Sopenharmony_ci    }
1241cb0ef41Sopenharmony_ci
1251cb0ef41Sopenharmony_ci    // sort results by number of results found, then by number of hits
1261cb0ef41Sopenharmony_ci    // then by number of matching lines
1271cb0ef41Sopenharmony_ci
1281cb0ef41Sopenharmony_ci    // coverage is ignored here because the contents of results are
1291cb0ef41Sopenharmony_ci    // nondeterministic due to either glob or readFiles or Object.entries
1301cb0ef41Sopenharmony_ci    return results.sort(/* istanbul ignore next */ (a, b) =>
1311cb0ef41Sopenharmony_ci      a.found.length > b.found.length ? -1
1321cb0ef41Sopenharmony_ci      : a.found.length < b.found.length ? 1
1331cb0ef41Sopenharmony_ci      : a.totalHits > b.totalHits ? -1
1341cb0ef41Sopenharmony_ci      : a.totalHits < b.totalHits ? 1
1351cb0ef41Sopenharmony_ci      : a.lines.length > b.lines.length ? -1
1361cb0ef41Sopenharmony_ci      : a.lines.length < b.lines.length ? 1
1371cb0ef41Sopenharmony_ci      : 0).slice(0, 10)
1381cb0ef41Sopenharmony_ci  }
1391cb0ef41Sopenharmony_ci
1401cb0ef41Sopenharmony_ci  formatResults (args, results) {
1411cb0ef41Sopenharmony_ci    const cols = Math.min(process.stdout.columns || Infinity, 80) + 1
1421cb0ef41Sopenharmony_ci
1431cb0ef41Sopenharmony_ci    const output = results.map(res => {
1441cb0ef41Sopenharmony_ci      const out = [res.cmd]
1451cb0ef41Sopenharmony_ci      const r = Object.keys(res.hits)
1461cb0ef41Sopenharmony_ci        .map(k => `${k}:${res.hits[k]}`)
1471cb0ef41Sopenharmony_ci        .sort((a, b) => a > b ? 1 : -1)
1481cb0ef41Sopenharmony_ci        .join(' ')
1491cb0ef41Sopenharmony_ci
1501cb0ef41Sopenharmony_ci      out.push(' '.repeat((Math.max(1, cols - out.join(' ').length - r.length - 1))))
1511cb0ef41Sopenharmony_ci      out.push(r)
1521cb0ef41Sopenharmony_ci
1531cb0ef41Sopenharmony_ci      if (!this.npm.config.get('long')) {
1541cb0ef41Sopenharmony_ci        return out.join('')
1551cb0ef41Sopenharmony_ci      }
1561cb0ef41Sopenharmony_ci
1571cb0ef41Sopenharmony_ci      out.unshift('\n\n')
1581cb0ef41Sopenharmony_ci      out.push('\n')
1591cb0ef41Sopenharmony_ci      out.push('-'.repeat(cols - 1) + '\n')
1601cb0ef41Sopenharmony_ci      res.lines.forEach((line, i) => {
1611cb0ef41Sopenharmony_ci        if (line === null || i > 3) {
1621cb0ef41Sopenharmony_ci          return
1631cb0ef41Sopenharmony_ci        }
1641cb0ef41Sopenharmony_ci
1651cb0ef41Sopenharmony_ci        const hilitLine = []
1661cb0ef41Sopenharmony_ci        for (const arg of args) {
1671cb0ef41Sopenharmony_ci          const finder = line.toLowerCase().split(arg.toLowerCase())
1681cb0ef41Sopenharmony_ci          let p = 0
1691cb0ef41Sopenharmony_ci          for (const f of finder) {
1701cb0ef41Sopenharmony_ci            hilitLine.push(line.slice(p, p + f.length))
1711cb0ef41Sopenharmony_ci            const word = line.slice(p + f.length, p + f.length + arg.length)
1721cb0ef41Sopenharmony_ci            const hilit = this.npm.chalk.bgBlack.red(word)
1731cb0ef41Sopenharmony_ci            hilitLine.push(hilit)
1741cb0ef41Sopenharmony_ci            p += f.length + arg.length
1751cb0ef41Sopenharmony_ci          }
1761cb0ef41Sopenharmony_ci        }
1771cb0ef41Sopenharmony_ci        out.push(hilitLine.join('') + '\n')
1781cb0ef41Sopenharmony_ci      })
1791cb0ef41Sopenharmony_ci
1801cb0ef41Sopenharmony_ci      return out.join('')
1811cb0ef41Sopenharmony_ci    }).join('\n')
1821cb0ef41Sopenharmony_ci
1831cb0ef41Sopenharmony_ci    const finalOut = results.length && !this.npm.config.get('long')
1841cb0ef41Sopenharmony_ci      ? 'Top hits for ' + (args.map(JSON.stringify).join(' ')) + '\n' +
1851cb0ef41Sopenharmony_ci      '—'.repeat(cols - 1) + '\n' +
1861cb0ef41Sopenharmony_ci      output + '\n' +
1871cb0ef41Sopenharmony_ci      '—'.repeat(cols - 1) + '\n' +
1881cb0ef41Sopenharmony_ci      '(run with -l or --long to see more context)'
1891cb0ef41Sopenharmony_ci      : output
1901cb0ef41Sopenharmony_ci
1911cb0ef41Sopenharmony_ci    return finalOut.trim()
1921cb0ef41Sopenharmony_ci  }
1931cb0ef41Sopenharmony_ci}
1941cb0ef41Sopenharmony_cimodule.exports = HelpSearch
195