1/* global Opal */
2const fs = require('fs')
3const ospath = require('path')
4const { once } = require('events')
5const asciidoctor = require('@asciidoctor/core')()
6const pkg = require('../package.json')
7const stdin = require('./stdin')
8
9const DOT_RELATIVE_RX = new RegExp(`^\\.{1,2}[/${ospath.sep.replace('/', '').replace('\\', '\\\\')}]`)
10
11class Invoker {
12  constructor (options) {
13    this.options = options
14  }
15
16  async invoke () {
17    const processArgs = this.options.argv.slice(2)
18    const { args } = this.options
19    const { verbose, version, files } = args
20    if (version || (verbose && processArgs.length === 1)) {
21      this.showVersion()
22      process.exit(0)
23    }
24    Invoker.prepareProcessor(args, asciidoctor)
25    const options = this.options.options
26    const failureLevel = options.failure_level
27    if (this.options.stdin) {
28      await Invoker.convertFromStdin(options, args)
29      await Invoker.exit(failureLevel, options)
30    } else if (files && files.length > 0) {
31      Invoker.processFiles(files, verbose, args.timings, options)
32      await Invoker.exit(failureLevel, options)
33    } else {
34      this.showHelp()
35      process.exit(0)
36    }
37  }
38
39  showHelp () {
40    if (this.options.args.help === 'syntax') {
41      console.log(fs.readFileSync(ospath.join(__dirname, '..', 'data', 'reference', 'syntax.adoc'), 'utf8'))
42    } else {
43      this.options.yargs.showHelp()
44    }
45  }
46
47  showVersion () {
48    console.log(this.version())
49  }
50
51  version () {
52    const releaseName = process.release ? process.release.name : 'node'
53    return `Asciidoctor.js ${asciidoctor.getVersion()} (Asciidoctor ${asciidoctor.getCoreVersion()}) [https://asciidoctor.org]
54Runtime Environment (${releaseName} ${process.version} on ${process.platform})
55CLI version ${pkg.version}`
56  }
57
58  /**
59   * @deprecated Use {#showVersion}. Will be removed in version 4.0.
60   */
61  static printVersion () {
62    console.log(new Invoker().version())
63  }
64
65  static async readFromStdin () {
66    return stdin.read()
67  }
68
69  static async convertFromStdin (options, args) {
70    const data = await Invoker.readFromStdin()
71    if (args.timings) {
72      const timings = asciidoctor.Timings.create()
73      const instanceOptions = Object.assign({}, options, { timings })
74      Invoker.convert(asciidoctor.convert, data, instanceOptions)
75      timings.printReport(process.stderr, '-')
76    } else {
77      Invoker.convert(asciidoctor.convert, data, options)
78    }
79  }
80
81  static convert (processorFn, input, options) {
82    try {
83      processorFn.apply(asciidoctor, [input, options])
84    } catch (e) {
85      if (e && e.name === 'NotImplementedError' && e.message === `asciidoctor: FAILED: missing converter for backend '${options.backend}'. Processing aborted.`) {
86        console.error(`> Error: missing converter for backend '${options.backend}'. Processing aborted.`)
87        if (options.backend === 'docbook' || options.backend === 'docbook5') {
88          console.error('> You might want to run the following command to support this backend:')
89          console.error('> npm install @asciidoctor/docbook-converter')
90        } else {
91          console.error('> You might want to require a Node.js package with --require option to support this backend.')
92        }
93        process.exit(1)
94      }
95      throw e
96    }
97  }
98
99  static convertFile (file, options) {
100    Invoker.convert(asciidoctor.convertFile, file, options)
101  }
102
103  static processFiles (files, verbose, timings, options) {
104    for (const file of files) {
105      if (verbose) {
106        console.log(`converting file ${file}`)
107      }
108      if (timings) {
109        const timings = asciidoctor.Timings.create()
110        const instanceOptions = Object.assign({}, options, { timings })
111        Invoker.convertFile(file, instanceOptions)
112        timings.printReport(process.stderr, file)
113      } else {
114        Invoker.convertFile(file, options)
115      }
116    }
117  }
118
119  static requireLibrary (requirePath, cwd = process.cwd()) {
120    if (requirePath.charAt(0) === '.' && DOT_RELATIVE_RX.test(requirePath)) {
121      // NOTE require resolves a dot-relative path relative to current file; resolve relative to cwd instead
122      requirePath = ospath.resolve(requirePath)
123    } else if (!ospath.isAbsolute(requirePath)) {
124      // NOTE appending node_modules prevents require from looking elsewhere before looking in these paths
125      const paths = [cwd, ospath.dirname(__dirname)].map((start) => ospath.join(start, 'node_modules'))
126      requirePath = require.resolve(requirePath, { paths })
127    }
128    return require(requirePath)
129  }
130
131  static prepareProcessor (argv, asciidoctor) {
132    const requirePaths = argv.require
133    if (requirePaths) {
134      requirePaths.forEach((requirePath) => {
135        const lib = Invoker.requireLibrary(requirePath)
136        if (lib && typeof lib.register === 'function') {
137          // REMIND: it could be an extension or a converter.
138          // the register function on a converter does not take any argument
139          // but the register function on an extension expects one argument (the extension registry)
140          // Until we revisit the API for extension and converter, we pass the registry as the first argument
141          lib.register(asciidoctor.Extensions)
142        }
143      })
144    }
145  }
146
147  static async exit (failureLevel, options = {}) {
148    let code = 0
149    const logger = asciidoctor.LoggerManager.getLogger()
150    if (logger && typeof logger.getMaxSeverity === 'function' && logger.getMaxSeverity() && logger.getMaxSeverity() >= failureLevel) {
151      code = 1
152    }
153    if (options.to_file === Opal.gvars.stdout) {
154      await once(process.stdout.end(), 'close')
155    }
156    process.exit(code)
157  }
158}
159
160module.exports = Invoker
161module.exports.asciidoctor = asciidoctor
162