|
| 1 | +// SPDX-License-Identifier: PMPL-1.0-or-later |
| 2 | +// AffineScript port of Cli.res — A2ML CLI (prototype). |
| 3 | + |
| 4 | +module Cli; |
| 5 | + |
| 6 | +use Compat; |
| 7 | +use A2ml; |
| 8 | +use Json; |
| 9 | + |
| 10 | +extern fn argv() -> [String] = "process" "argv"; |
| 11 | +extern fn read_text(path: String) -> String = "platform" "readTextFile"; |
| 12 | +extern fn write_text(path: String, text: String) -> Unit = "platform" "writeTextFile"; |
| 13 | +extern fn read_stdin() -> String = "platform" "readStdin"; |
| 14 | +extern fn exit(code: Int) -> Unit = "platform" "exit"; |
| 15 | + |
| 16 | +pub let help_text = "A2ML CLI (prototype)\n\n" |
| 17 | + ++ "Usage:\n a2ml <render|validate|ast> <file...|-> [options]\n\n" |
| 18 | + ++ "Commands:\n render Render HTML to stdout (or --out)\n" |
| 19 | + ++ " validate Validate in checked mode, exit 2 on errors\n" |
| 20 | + ++ " ast Output JSON surface AST\n\n" |
| 21 | + ++ "Options:\n --mode <lax|checked> Parse mode (default: lax)\n" |
| 22 | + ++ " --out <path> Write output to file\n" |
| 23 | + ++ " --concat Concatenate outputs when multiple inputs\n" |
| 24 | + ++ " --stdin Read input from stdin (equivalent to '-')\n" |
| 25 | + ++ " -h, --help Show this help\n\n" |
| 26 | + ++ "Notes:\n * Use '-' as a filename to read from stdin.\n" |
| 27 | + ++ " * For multiple files, --concat joins outputs in order.\n"; |
| 28 | + |
| 29 | +pub fn usage() -> Effect[IO] Unit { |
| 30 | + Compat.console_log(help_text); |
| 31 | + exit(1) |
| 32 | +} |
| 33 | + |
| 34 | +pub fn get_arg(args: [String], name: String) -> Option<String> { |
| 35 | + let i = 0; |
| 36 | + while i < len(args) { |
| 37 | + if args[i] == name { |
| 38 | + if i + 1 < len(args) { return Some(args[i + 1]); } else { return None; } |
| 39 | + } |
| 40 | + i = i + 1; |
| 41 | + } |
| 42 | + None |
| 43 | +} |
| 44 | + |
| 45 | +pub fn has_flag(args: [String], name: String) -> Bool { |
| 46 | + let i = 0; |
| 47 | + while i < len(args) { if args[i] == name { return true; } i = i + 1; } |
| 48 | + false |
| 49 | +} |
| 50 | + |
| 51 | +pub fn collect_inputs(args: [String]) -> [String] { |
| 52 | + let inputs = []; |
| 53 | + let i = 3; |
| 54 | + let done = false; |
| 55 | + while i < len(args) && !done { |
| 56 | + if Compat.starts_with(args[i], "-") { |
| 57 | + done = true; |
| 58 | + } else { |
| 59 | + inputs = inputs ++ [args[i]]; |
| 60 | + i = i + 1; |
| 61 | + } |
| 62 | + } |
| 63 | + inputs |
| 64 | +} |
| 65 | + |
| 66 | +pub fn read_input(path: String) -> String { |
| 67 | + if path == "-" { read_stdin() } else { read_text(path) } |
| 68 | +} |
| 69 | + |
| 70 | +pub fn render_doc(input: String, mode: A2ml.ParseMode) -> String { |
| 71 | + A2ml.render_html(A2ml.parse(mode, input)) |
| 72 | +} |
| 73 | + |
| 74 | +pub fn validate_doc(input: String, mode: A2ml.ParseMode) -> [A2ml.ParseError] { |
| 75 | + let doc = A2ml.parse(mode, input); |
| 76 | + if mode == A2ml.Checked { A2ml.validate_checked(doc) } else { [] } |
| 77 | +} |
| 78 | + |
| 79 | +pub fn ast_doc(input: String, mode: A2ml.ParseMode) -> String { |
| 80 | + Json.doc_to_json(A2ml.parse(mode, input)) |
| 81 | +} |
| 82 | + |
| 83 | +pub fn main() -> Effect[IO] Unit { |
| 84 | + let args = argv(); |
| 85 | + if has_flag(args, "-h") || has_flag(args, "--help") { Compat.console_log(help_text); exit(0); } |
| 86 | + if len(args) < 3 { usage(); } |
| 87 | + |
| 88 | + let command = args[2]; |
| 89 | + let inputs = collect_inputs(args); |
| 90 | + let read_from_stdin = has_flag(args, "--stdin"); |
| 91 | + let mode = match get_arg(args, "--mode") { Some("checked") => A2ml.Checked, _ => A2ml.Lax }; |
| 92 | + let out_path = get_arg(args, "--out"); |
| 93 | + let concat = has_flag(args, "--concat"); |
| 94 | + let sources = if read_from_stdin { ["-"] } else { inputs }; |
| 95 | + if len(sources) == 0 { usage(); } |
| 96 | + |
| 97 | + match command { |
| 98 | + "render" => { |
| 99 | + let outputs = []; |
| 100 | + let i = 0; |
| 101 | + while i < len(sources) { outputs = outputs ++ [render_doc(read_input(sources[i]), mode)]; i = i + 1; } |
| 102 | + let result = if concat { Compat.array_join(outputs, "\n") } else { Compat.array_join(outputs, "\n\n") }; |
| 103 | + match out_path { Some(p) => write_text(p, result), None => Compat.console_log(result) } |
| 104 | + } |
| 105 | + "validate" => { |
| 106 | + let all_errors = []; |
| 107 | + let i = 0; |
| 108 | + while i < len(sources) { |
| 109 | + let errs = validate_doc(read_input(sources[i]), mode); |
| 110 | + let j = 0; |
| 111 | + while j < len(errs) { all_errors = all_errors ++ [errs[j]]; j = j + 1; } |
| 112 | + i = i + 1; |
| 113 | + } |
| 114 | + if len(all_errors) == 0 { |
| 115 | + Compat.console_log("ok") |
| 116 | + } else { |
| 117 | + let k = 0; |
| 118 | + while k < len(all_errors) { |
| 119 | + Compat.console_log(Compat.int_to_string(all_errors[k].line) ++ ": " ++ all_errors[k].msg); |
| 120 | + k = k + 1; |
| 121 | + } |
| 122 | + exit(2) |
| 123 | + } |
| 124 | + } |
| 125 | + "ast" => { |
| 126 | + let outputs = []; |
| 127 | + let i = 0; |
| 128 | + while i < len(sources) { outputs = outputs ++ [ast_doc(read_input(sources[i]), mode)]; i = i + 1; } |
| 129 | + let result = Compat.array_join(outputs, "\n"); |
| 130 | + match out_path { Some(p) => write_text(p, result), None => Compat.console_log(result) } |
| 131 | + } |
| 132 | + _ => usage(), |
| 133 | + } |
| 134 | +} |
| 135 | + |
| 136 | +main() |
0 commit comments