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