// The `asciidoctor` command line interface (Ruby `Asciidoctor::Cli`): option
// parsing is in options.mbt; this file is the invoker (Ruby `Cli::Invoker`).
// The conversion itself (Ruby `Asciidoctor.convert`) is `@io.convert_source`.
///|
async fn write_err(s : String) -> Unit {
@stdio.stderr.write(s)
}
///|
async fn main {
// server-side syntax highlighting (`-a source-highlighter=pygments`)
@pygments.register()
let args = @env.args()
let code = run(args[1:].to_owned())
if code != 0 {
c_exit(code)
}
}
///|
/// Runs the command line and returns the exit code.
async fn run(args : Array[String]) -> Int {
let command = parse_args(args) catch {
CliError(msg) => {
write_err(msg + "\n")
@stdio.stdout.write(usage)
return 1
}
}
match command {
Help(topic) => help(topic)
Version => {
@stdio.stdout.write(version_text())
0
}
Usage => {
write_err(usage)
1
}
Convert(opts) => {
for notice in opts.notices {
write_err(notice + "\n")
}
match check_inputs(opts) {
Some(msg) => {
write_err(msg)
return 1
}
None => ()
}
invoke(opts)
}
}
}
///|
/// `-h TOPIC`: the man page and the syntax overview are not bundled with the
/// port, so those topics fail like Ruby does when the files are missing.
async fn help(topic : String?) -> Int {
match topic {
Some("manpage") =>
match @env.get_env_var("ASCIIDOCTOR_MANPAGE_PATH") {
Some(path) if !path.has_suffix(".gz") && @fs.exists(path) => {
@stdio.stdout.write(@rb.chomp(@fs.read_file(path).text()) + "\n")
0
}
Some(path) => {
write_err("asciidoctor: FAILED: manual page not found: \{path}\n")
1
}
None => {
write_err(
"asciidoctor: FAILED: manual page not found; try `man asciidoctor`\n",
)
1
}
}
Some("syntax") => {
write_err(
"asciidoctor: FAILED: syntax page not found; visit https://asciidoctor.org/docs\n",
)
1
}
_ => {
@stdio.stdout.write(usage)
0
}
}
}
///|
/// Checks the input files and the options the port cannot honor (Ruby
/// `Options#parse!` after parsing). Returns the error to print.
async fn check_inputs(opts : CliOptions) -> String? {
for input in opts.input_files {
guard input != "-" else { continue }
if !@fs.exists(input) {
return Some("asciidoctor: FAILED: input file \{input} is missing\n")
}
match @fs.kind(input) {
Regular | Pipe =>
if !@fs.can_read(input) {
return Some(
"asciidoctor: FAILED: input file \{input} is not readable\n",
)
}
kind => {
let ftype = match kind {
Directory => "directory"
CharDevice => "characterSpecial"
BlockDevice => "blockSpecial"
Socket => "socket"
SymLink => "link"
_ => "unknown"
}
return Some(
"asciidoctor: FAILED: input path \{input} is a \{ftype}, not a file\n",
)
}
}
}
let trace_hint = if opts.trace {
""
} else {
" Use --trace to show backtrace\n"
}
if !opts.template_dirs.is_empty() {
// Ruby: the tilt gem is required; the port has no template support
return Some(
"asciidoctor: FAILED: 'tilt' could not be loaded\n Custom converter templates are not supported by the MoonBit port\n\{trace_hint}",
)
}
// Ruby libraries (-r) cannot be loaded by the port
match opts.requires.get(0) {
Some(path) =>
Some("asciidoctor: FAILED: '\{path}' could not be loaded\n\{trace_hint}")
None => None
}
}
///|
/// The message Ruby's invoker prints for an error that aborted the run.
fn error_message(e : Error) -> String {
match e {
// Ruby RuntimeErrors are printed with their class
@core.ConversionFailed(msg) => "\{msg} (RuntimeError)"
@core.SecurityError(msg) => msg
e => e.to_string()
}
}
///|
/// Converts the input files (Ruby `Invoker#invoke!`) and returns the exit code.
async fn invoke(opts : CliOptions) -> Int {
let infiles = opts.input_files
let mut outfile = opts.output_file
let mut stdin = false
if infiles.length() == 1 {
if infiles[0] == "-" {
outfile = Some(outfile.unwrap_or("-"))
stdin = true
} else if (@fs.kind(infiles[0]) catch { _ => Regular }) is Pipe {
outfile = Some(outfile.unwrap_or("-"))
}
}
// every file output creates the missing directories
let (destination, mkdirs) : (@io.Destination, Bool) = match outfile {
Some("-") => (Stdout, false)
Some(f) => (File(f), true)
None => (Default, true)
}
let messages : Array[@core.LogMessage] = []
let logger = if opts.verbose == 0 {
@core.Logger::null()
} else {
let default_level : @core.Severity = if opts.verbose == 2 {
Debug
} else {
Warn
}
@core.Logger::new(
m => messages.push(m),
level=opts.log_level.unwrap_or(default_level),
)
}
@core.set_logger(logger) |> ignore
@core.set_utc_offset(@localtime.utc_offset)
match @env.get_env_var("SOURCE_DATE_EPOCH") {
Some(s) => @core.set_source_date_epoch(Some(@rb.to_i(s).to_int64()))
None => ()
}
let flush_messages = async fn() {
for m in messages {
write_err(@core.format_log_message(m) + "\n")
}
messages.clear()
}
let new_options = fn() {
@core.Options::new(
attributes=opts.attributes.map(a => (a.0, @core.AttrVal::Str(a.1))),
safe=opts.safe,
standalone=opts.standalone,
base_dir?=opts.base_dir,
sourcemap=opts.sourcemap,
timings?=if opts.timings {
Some(@core.Timings::new(monotonic_seconds))
} else {
None
},
)
}
let cwd = @env.current_dir().unwrap_or("/")
let abs_srcdir = opts.source_dir.map(d => @core.expand_path_from(cwd, d))
let convert_one = async fn(source : @io.Source, subject : String) {
let options = new_options()
let mut to_dir = opts.destination_dir
match (source, abs_srcdir, to_dir) {
(Path(path), Some(srcdir), Some(d)) => {
let abs_indir = @core.dirname(@core.expand_path_from(cwd, path))
if abs_indir.has_prefix(srcdir + "/") {
to_dir = Some(d + @rb.from(abs_indir, srcdir.length()))
}
}
_ => ()
}
@io.convert_source(source, options~, destination~, to_dir?, mkdirs~)
|> ignore
flush_messages()
if options.timings is Some(t) {
write_err(t.report(subject~))
}
}
try {
if stdin {
let text = @stdio.stdin.read_all().text()
convert_one(Text(text), "-")
} else {
for infile in infiles {
convert_one(Path(infile), infile)
}
}
} catch {
e => {
flush_messages()
write_err(error_message(e) + "\n")
if !opts.trace {
write_err(" Use --trace to show backtrace\n")
}
return 1
}
}
match logger.max_severity {
Some(s) if s.to_int() >= opts.failure_level.to_int() => 1
_ => 0
}
}
///|
#cfg(target="native")
extern "C" fn c_exit(code : Int) = "exit"
///|
#cfg(target="wasm")
fn c_exit(code : Int) = "wasi_snapshot_preview1" "proc_exit"