///|
/// `pygmentize`: highlight an input file and write the result to an output
/// file (a port of `pygments/cmdline.py`).
async fn main {
let argv = @env.args()
let code = run(argv[1:].to_owned()) catch {
e => {
eprint("\n*** Error while highlighting:\n\{error_message(e)}\n")
eprint("*** If this is a bug you want to report, please rerun with -v.\n")
1
}
}
@sys.exit(code)
}
///|
async fn eprint(s : String) -> Unit {
@stdio.stderr.write(s)
}
///|
async fn print(s : String) -> Unit {
@stdio.stdout.write(s)
}
///|
fn error_message(e : Error) -> String {
match e {
@lexers.ClassNotFound(msg) => msg
@lexer.OptionError(msg) => msg
@formatters.ClassNotFound(msg) => msg
@formatters.FormatterError(msg) => msg
_ => Repr(e).to_string()
}
}
///|
/// Python's `str(OSError)`: `[Errno N] message: 'path'`.
fn os_error_message(e : Error, path : String) -> String {
match e {
@os_error.OSError(errno, ..) as e =>
"[Errno \{errno}] \{os_message(e)}: \{@pystr.repr(path)}"
_ => error_message(e)
}
}
///|
/// Decodes input bytes like Python's `guess_decode`: UTF-8 (dropping a BOM),
/// falling back to Latin-1.
fn guess_decode(data : Bytes) -> String {
@utf8.decode(data, ignore_bom=true) catch {
_ => {
let sb = StringBuilder()
for b in data {
sb.write_char(b.to_int().unsafe_to_char())
}
sb.to_string()
}
}
}
///|
async fn read_stdin() -> String {
guess_decode(@stdio.stdin.read_all().binary())
}
///|
async fn usage_error(msg : String) -> Int32 {
eprint(usage)
eprint("pygmentize: error: \{msg}\n")
2
}
///|
async fn print_usage_help() -> Int32 {
eprint(usage + help_text)
2
}
///|
async fn run(argv : Array[String]) -> Int32 {
let args = parse_args(argv) catch {
UsageError(msg) => return usage_error(msg)
}
if args.help {
print(usage + help_text)
return 0
}
if args.version {
print(version_line + "\n")
return 0
}
// -L
if args.list is Some(what) {
if args.has_other_than("L") && !(args.json && !args.has_other_than("json")) {
if [
args.lexer is Some(_),
args.guess,
args.formatter is Some(_),
args.input is Some(_),
args.output is Some(_),
!args.options.is_empty(),
]
.iter()
.any(x => x) {
return print_usage_help()
}
}
let kinds = what.map(w => {
if w.has_suffix("s") {
w.unsafe_substring(start=0, end=w.length() - 1)
} else {
w
}
})
let allowed = ["lexer", "formatter", "filter", "style"]
if kinds.iter().any(k => !allowed.contains(k)) {
return print_usage_help()
}
let kinds = if kinds.is_empty() { allowed } else { kinds }
if args.json {
print(list_as_json(kinds))
} else {
print(version_line + "\n")
for k in kinds {
print(list_what(k))
}
}
return 0
}
// -H
if args.help_on is Some((what, name)) {
if args.has_other_than("H") {
return print_usage_help()
}
if !["lexer", "formatter", "filter"].contains(what) {
return print_usage_help()
}
match help_on(what, name) {
Some(text) => {
print(text)
return 0
}
None => {
eprint("\{what} not found!\n")
return 1
}
}
}
// -O and -P
let options : Map[String, String] = Map([])
parse_options(args.options, options)
for p in args.single_options {
match p.find("=") {
Some(i) =>
options[p.unsafe_substring(start=0, end=i)] = p.unsafe_substring(
start=i + 1,
end=p.length(),
)
None => options[p] = "True"
}
}
// -N
if args.name_for_file is Some(fname) {
let aliases = match @lexers.find_lexer_class_for_filename(fname) {
Some(entry) => entry.0.aliases
None => @lexers.text_lexer_info.aliases
}
print(aliases[0] + "\n")
return 0
}
// -C
if args.guess_stdin {
let text = read_stdin()
let aliases = @lexers.guess_lexer(text).info.aliases catch {
_ => @lexers.text_lexer_info.aliases
}
print(aliases[0] + "\n")
return 0
}
// -S
if args.style is Some(style) {
guard args.formatter is Some(f) else { return print_usage_help() }
if args.lexer is Some(_) || args.input is Some(_) {
return print_usage_help()
}
options["style"] = style
let fmter = @formatters.get_formatter_by_name(f, options~) catch {
e => {
eprint(error_message(e) + "\n")
return 1
}
}
print(fmter.get_style_defs(arg=args.style_arg.unwrap_or("")) + "\n")
return 0
}
if args.style_arg is Some(_) {
return print_usage_help()
}
let filters = parse_filters(args.filters)
if args.custom {
eprint(
"Error: custom lexers and formatters (-x) are not supported by the MoonBit port\n",
)
return 1
}
// select lexer
let mut lexer : @lexer.Lexer? = None
if args.lexer is Some(name) {
lexer = Some(
@lexers.get_lexer_by_name(name, options~) catch {
e => {
eprint("Error: \{error_message(e)}\n")
return 1
}
},
)
}
// read input
let mut code = ""
match args.input {
Some(infn) => {
if args.stream {
eprint("Error: -s option not usable when input file specified\n")
return 2
}
let data = @fs.read_file(infn) catch {
e => {
eprint("Error: cannot read infile: \{os_error_message(e, infn)}\n")
return 1
}
}
code = guess_decode(data.binary())
if lexer is None {
lexer = Some(
@lexers.get_lexer_for_filename(infn, code~, options~) catch {
@lexers.ClassNotFound(_) if args.guess =>
@lexers.guess_lexer(code, options~) catch {
_ => @lexers.text_lexer(options~)
}
e => {
eprint("Error: \{error_message(e)}\n")
return 1
}
},
)
}
}
None =>
if !args.stream {
code = read_stdin()
if lexer is None {
lexer = Some(
@lexers.guess_lexer(code, options~) catch {
_ => @lexers.text_lexer(options~)
},
)
}
} else if lexer is None {
eprint("Error: when using -s a lexer has to be selected with -l\n")
return 2
}
}
let lexer = lexer.unwrap()
// filters
for f in filters {
let filter = @filters.get_filter_by_name(f.0, options=f.1) catch {
e => {
eprint("Error: \{error_message(e)}\n")
return 1
}
}
lexer.add_filter(filter)
}
// select formatter; like Python, the output encoding defaults to the
// input/terminal encoding (always UTF-8 here)
let fopts = options.copy()
if !fopts.contains("outencoding") && !fopts.contains("encoding") {
fopts["outencoding"] = "utf-8"
}
let options = fopts
let fmter = match args.formatter {
Some(name) =>
@formatters.get_formatter_by_name(name, options~) catch {
e => {
eprint("Error: \{error_message(e)}\n")
return 1
}
}
None =>
match args.output {
Some(outfn) =>
@formatters.get_formatter_for_filename(outfn, options~) catch {
e => {
eprint("Error: \{error_message(e)}\n")
return 1
}
}
None => default_terminal_formatter(options)
}
}
// LaTeX `escapeinside`: collect escaped text before the language lexer
let lexer = match options.get("escapeinside") {
Some(esc) if esc.length() == 2 && fmter.name() == "LaTeX" =>
@formatters.latex_embedded_lexer(
esc.unsafe_substring(start=0, end=1),
esc.unsafe_substring(start=1, end=2),
lexer,
)
_ => lexer
}
if !args.stream {
let out = @formatters.highlight(code, lexer, fmter)
write_aux_files(fmter, args.output)
match args.output {
Some(outfn) =>
@fs.write_file(outfn, out, permission=0o644) catch {
e => {
eprint(
"Error: cannot open outfile: \{os_error_message(e, outfn)}\n",
)
return 1
}
}
None => print(out)
}
return 0
}
// -s: line by line
let buffered = StringBuilder()
while @stdio.stdin.read_until("\n") is Some(line) {
let out = fmter.format(lexer.get_tokens(line + "\n"))
match args.output {
Some(_) => buffered.write_string(out)
None => print(out)
}
}
match args.output {
Some(outfn) => @fs.write_file(outfn, buffered.to_string())
None => ()
}
0
}
///|
/// Writes side files of a formatter (the HTML `cssfile`), relative to the
/// output file's directory like Python's `HtmlFormatter`.
async fn write_aux_files(
fmter : @formatters.Formatter,
output : String?,
) -> Unit {
for aux in fmter.aux_files() {
let path = if aux.path.has_prefix("/") {
aux.path
} else {
match output {
Some(outfn) =>
match outfn.rev_find("/") {
Some(i) => outfn.unsafe_substring(start=0, end=i + 1) + aux.path
None => aux.path
}
None => {
eprint(
"Note: Cannot determine output file name, using current directory as base for the CSS file name\n",
)
aux.path
}
}
}
if aux.noclobber && @fs.exists(path) {
continue
}
@fs.write_file(path, aux.contents, permission=0o644)
}
}
///|
/// The formatter used for terminal output when none is given: true color,
/// 256 colors or 16 colors depending on `COLORTERM`/`TERM`.
fn default_terminal_formatter(
options : Map[String, String],
) -> @formatters.Formatter raise {
let colorterm = @env.get_env_var("COLORTERM").unwrap_or("")
let term = @env.get_env_var("TERM").unwrap_or("")
let name = if colorterm == "truecolor" || colorterm == "24bit" {
"terminal16m"
} else if term.contains("256") {
"terminal256"
} else {
"terminal"
}
@formatters.get_formatter_by_name(name, options~)
}
///|
/// The system message of an `OSError` ("No such file or directory").
fn os_message(e : @os_error.OSError) -> String {
let s = e.to_string()
let msg = match s.rev_find(": ") {
Some(i) => s.unsafe_substring(start=i + 2, end=s.length())
None => s
}
// `to_string` renders `OSError("context: message")`
if msg.has_suffix("\")") {
msg.unsafe_substring(start=0, end=msg.length() - 2)
} else {
msg
}
}