1/* global Opal */
2const Yargs = require('yargs/yargs')
3const asciidoctor = require('@asciidoctor/core')()
4
5const convertOptions = (args, attrs) => {
6  const attributes = attrs || []
7  const backend = args.backend
8  const doctype = args.doctype
9  const safeMode = args['safe-mode']
10  const embedded = args.embedded === true || args['no-header-footer'] === true || args.standalone === false
11  const standalone = !embedded
12  const sectionNumbers = args['section-numbers']
13  const baseDir = args['base-dir']
14  const destinationDir = args['destination-dir']
15  const outFile = args['out-file']
16  const templateDir = args['template-dir']
17  const templateEngine = args['template-engine']
18  const quiet = args.quiet
19  const verbose = args.verbose
20  const timings = args.timings
21  const trace = args.trace
22  const requireLib = args.require
23  let level = args['failure-level'].toUpperCase()
24  if (level === 'WARNING') {
25    level = 'WARN'
26  }
27  const failureLevel = asciidoctor.LoggerSeverity[level]
28  const debug = verbose && args['verbose-sole-argument'] !== true
29  if (debug) {
30    console.log('require ' + requireLib)
31    console.log('backend ' + backend)
32    console.log('doctype ' + doctype)
33    console.log('standalone ' + standalone)
34    console.log('section-numbers ' + sectionNumbers)
35    console.log('failure-level ' + level)
36    console.log('quiet ' + quiet)
37    console.log('verbose ' + verbose)
38    console.log('timings ' + timings)
39    console.log('trace ' + trace)
40    console.log('base-dir ' + baseDir)
41    console.log('destination-dir ' + destinationDir)
42    console.log('template-dir ' + templateDir)
43    console.log('template-engine ' + templateEngine)
44  }
45  const verboseMode = quiet ? 0 : verbose ? 2 : 1
46  if (sectionNumbers) {
47    attributes.push('sectnums')
48  }
49  const cliAttributes = args.attribute
50  if (cliAttributes) {
51    attributes.push(...cliAttributes)
52  }
53  if (debug) {
54    console.log('verbose-mode ' + verboseMode)
55    console.log('attributes ' + attributes)
56  }
57  const options = {
58    doctype,
59    safe: safeMode,
60    standalone,
61    failure_level: failureLevel,
62    verbose: verboseMode,
63    timings,
64    trace
65  }
66  if (backend) {
67    options.backend = backend
68  }
69  if (baseDir != null) {
70    options.base_dir = baseDir
71  }
72  if (destinationDir != null) {
73    options.to_dir = destinationDir
74  }
75  if (templateDir) {
76    options.template_dirs = templateDir
77  }
78  if (templateEngine) {
79    options.template_engine = templateEngine
80  }
81  if (typeof outFile !== 'undefined') {
82    if (outFile === '') {
83      options.to_file = '-'
84    } else if (outFile === '\'\'') {
85      options.to_file = '-'
86    } else {
87      options.to_file = outFile
88      options.mkdirs = true
89    }
90  } else {
91    options.mkdirs = true
92  }
93  options.attributes = attributes
94  if (debug) {
95    console.log('options ' + JSON.stringify(options))
96  }
97  if (options.to_file === '-') {
98    options.to_file = Opal.gvars.stdout
99  }
100  return options
101}
102
103class Options {
104  constructor (options) {
105    this.options = options || {}
106    this.args = {
107      standalone: typeof this.options.standalone !== 'undefined' ? this.options.standalone : true,
108      backend: this.options.backend,
109      'safe-mode': typeof this.options.safe !== 'undefined' ? this.options.safe : 'unsafe'
110    }
111    if (Array.isArray(this.options.attributes)) {
112      this.attributes = options.attributes
113    } else if (typeof this.options.attributes === 'object') {
114      const attrs = this.options.attributes
115      const attributes = []
116      Object.keys(attrs).forEach((key) => {
117        attributes.push(`${key}=${attrs[key]}`)
118      })
119      this.attributes = attributes
120    } else {
121      this.attributes = []
122    }
123    const yargs = Yargs()
124    this.cmd = yargs
125      .option('backend', {
126        alias: 'b',
127        describe: 'set output format backend',
128        type: 'string'
129      })
130      .option('doctype', {
131        alias: 'd',
132        describe: 'document type to use when converting document',
133        choices: ['article', 'book', 'manpage', 'inline']
134      })
135      .option('out-file', {
136        alias: 'o',
137        describe: 'output file (default: based on path of input file) use \'\' to output to STDOUT',
138        type: 'string'
139      })
140      .option('safe-mode', {
141        alias: 'S',
142        describe: 'set safe mode level explicitly, disables potentially dangerous macros in source files, such as include::[]',
143        choices: ['unsafe', 'safe', 'server', 'secure']
144      })
145      .option('embedded', {
146        alias: 'e',
147        describe: 'suppress enclosing document structure and output an embedded document',
148        type: 'boolean'
149      })
150      .option('no-header-footer', {
151        alias: 's',
152        describe: 'suppress enclosing document structure and output an embedded document',
153        type: 'boolean'
154      })
155      .option('section-numbers', {
156        alias: 'n',
157        default: false,
158        describe: 'auto-number section titles in the HTML backend disabled by default',
159        type: 'boolean'
160      })
161      .option('base-dir', {
162        // QUESTION: should we check that the directory exists ? coerce to a directory ?
163        alias: 'B',
164        describe: 'base directory containing the document and resources (default: directory of source file)',
165        type: 'string'
166      })
167      .option('destination-dir', {
168        // QUESTION: should we check that the directory exists ? coerce to a directory ?
169        alias: 'D',
170        describe: 'destination output directory (default: directory of source file)',
171        type: 'string'
172      })
173      .option('failure-level', {
174        default: 'FATAL',
175        describe: 'set minimum logging level that triggers non-zero exit code',
176        choices: ['info', 'INFO', 'warn', 'WARN', 'warning', 'WARNING', 'error', 'ERROR', 'fatal', 'FATAL']
177      })
178      .option('quiet', {
179        alias: 'q',
180        default: false,
181        describe: 'suppress warnings',
182        type: 'boolean'
183      })
184      .option('trace', {
185        default: false,
186        describe: 'include backtrace information on errors',
187        type: 'boolean'
188      })
189      .option('verbose', {
190        alias: 'v',
191        default: false,
192        describe: 'enable verbose mode',
193        type: 'boolean'
194      })
195      .option('timings', {
196        alias: 't',
197        default: false,
198        describe: 'enable timings mode',
199        type: 'boolean'
200      })
201      .option('template-dir', {
202        alias: 'T',
203        array: true,
204        describe: 'a directory containing custom converter templates that override the built-in converter (may be specified multiple times)',
205        type: 'string'
206      })
207      .option('template-engine', {
208        alias: 'E',
209        describe: 'template engine to use for the custom converter templates',
210        type: 'string'
211      })
212      .option('attribute', {
213        alias: 'a',
214        array: true,
215        describe: 'a document attribute to set in the form of key, key! or key=value pair',
216        type: 'string'
217      })
218      .option('require', {
219        alias: 'r',
220        array: true,
221        describe: 'require the specified library before executing the processor, using the standard Node require',
222        type: 'string'
223      })
224      .version(false)
225      .option('version', {
226        alias: 'V',
227        default: false,
228        describe: 'display the version and runtime environment (or -v if no other flags or arguments)',
229        type: 'boolean'
230      })
231      .help(false)
232      .option('help', {
233        describe: `print a help message
234show this usage if TOPIC is not specified or recognized
235show an overview of the AsciiDoc syntax if TOPIC is syntax`,
236        type: 'string'
237      })
238      .nargs('template-dir', 1)
239      .nargs('attribute', 1)
240      .nargs('require', 1)
241      .usage(`$0 [options...] files...
242Translate the AsciiDoc source file or file(s) into the backend output format (e.g., HTML 5, DocBook 5, etc.)
243By default, the output is written to a file with the basename of the source file and the appropriate extension`)
244      .example('$0 -b html5 doc.asciidoc', 'convert an AsciiDoc file to HTML5; result will be written in a file named doc.html')
245      .epilogue('For more information, please visit https://asciidoctor.org/docs')
246    this.yargs = yargs
247  }
248
249  parse (argv) {
250    const processArgs = argv.slice(2)
251    this.argv = argv
252    const args = this.argsParser().parse(processArgs)
253    Object.assign(this.args, args)
254    const files = this.args.files
255    this.stdin = files && files.length === 0 && processArgs[processArgs.length - 1] === '-'
256    if (this.stdin) {
257      this.args['out-file'] = this.args['out-file'] || '-'
258    }
259    this.args['verbose-sole-argument'] = this.args.verbose && processArgs.length === 1
260    const options = convertOptions(this.args, this.attributes)
261    Object.assign(this.options, options)
262    return this
263  }
264
265  addOption (key, opt) {
266    this.cmd.option(key, opt)
267    return this
268  }
269
270  argsParser () {
271    return this.yargs
272      .detectLocale(false)
273      .wrap(Math.min(120, this.yargs.terminalWidth()))
274      .command('$0 [files...]', '', () => this.cmd)
275      .parserConfiguration({
276        'boolean-negation': false
277      })
278  }
279}
280
281module.exports = Options
282