
Arguments.pudu
Pudu146 lines6.6 KB
1/** @Docgen.Command.Arguments — command lines read into commands, paths, and options */2module PuduLangDocgen.Command.Arguments34import Std.Path as Path5import PuduLangDocgen.Constants.Package as Package67/** @Docgen.Command.Parsed — a command, its positional arguments, and its options */8export type Parsed = { command: Str, positional: Array[Str], flags: Array[Str], values: Array[(Str, Str)] }91011const VALUED: Array[(Str, Str)] = [12 ("--output", "-o"), ("--metadata", "-m"), ("--xref", "-x"), ("--template", "-t"), ("--theme", ""), ("--port", "-p"),13 ("--hostname", "-n"), ("--log", "-l"), ("--logLevel", ""), ("--open-file", "")14]151617const FLAGS: Array[(Str, Str)] = [18 ("--force", ""), ("--dryRun", ""), ("--warningsAsErrors", ""), ("--disableGitFeatures", ""), ("--exportRawModel", ""),19 ("--exportViewModel", ""), ("--serve", "-s"), ("--watch", "-w"), ("--open-browser", ""), ("--yes", "-y"), ("--all", "-a"),20 ("--verbose", ""), ("--help", "-h")21]222324const COMMAND_WORDS: Array[Str] = ["build", "metadata", "serve", "init", "pdf", "download", "merge", "template", "version", "help"]252627const COMMANDS: Array[(Str, Str)] = [28 ("[config]", "Build the site, API reference included, and PDFs when a table enables them"),29 ("build [config]", "Build the site"),30 ("metadata [config]", "Write API metadata files for the configured sources"),31 ("pdf [config]", "Build the site and a PDF for every table of contents that sets pdf"),32 ("serve [folder]", "Preview a built site over HTTP"),33 ("init [folder]", "Create a new documentation project"),34 ("template list | export [name]", "List the built-in templates or copy one for customizing"),35 ("download <file> --xref <address>", "Save a cross-reference map from the web"),36 ("merge <maps...> -o <file>", "Combine cross-reference maps into one"),37 ("version", "Print the package version"),38 ("help", "Print this help")39]404142const OPTIONS: Array[Str] = [43 " -o, --output <folder> Output folder instead of the configured one",44 " -m, --metadata <key=value> Global metadata; text, or true/false",45 " -x, --xref <map> Another cross-reference map address or path",46 " -t, --template <folder> Another template folder",47 " --theme <folder> Another template folder, applied after templates",48 " --force Rewrite every output file",49 " --dryRun Validate and plan without writing",50 " --warningsAsErrors Fail on any warning",51 " --disableGitFeatures Skip dates and edit links from git",52 " --exportRawModel Write each page's data model beside it",53 " --exportViewModel Write each page's view model beside it",54 " -l, --log <file> Save every diagnostic as JSON",55 " --logLevel <level> Print error, warning, info, verbose, or diagnostic messages",56 " --verbose Print verbose messages and the files written",57 " -s, --serve Preview the site after building",58 " -w, --watch Rebuild while serving when sources change",59 " -n, --hostname <host> Preview host (localhost)",60 " -p, --port <number> Preview port (8080)",61 " --open-browser Open the preview in the default browser",62 " --open-file <path> Open this site path in the browser",63 " -y, --yes Accept every default when creating a project",64 " -a, --all Export every built-in template",65 " -h, --help Print this help"66]676869export fn parse(arguments: &Array[Str]) -> Result[Parsed, Str] {70 var parsed = Parsed{command: "", positional: [], flags: [], values: []}71 var index = 072 if !arguments.isEmpty() && COMMAND_WORDS.contains(arguments[0]) {73 parsed = Parsed{..parsed, command: arguments[0]}74 index = 175 }76 while index < arguments.length() {77 let argument = arguments[index]78 let valued = longName(&VALUED, argument)79 let flag = longName(&FLAGS, argument)80 if !valued.isEmpty() {81 if index + 1 >= arguments.length() { return Err("option " + argument + " needs a value") }82 parsed = Parsed{..parsed, values: parsed.values.push((valued, arguments[index + 1]))}83 index = index + 284 } else if !flag.isEmpty() {85 parsed = Parsed{..parsed, flags: parsed.flags.push(flag)}86 index = index + 187 } else if argument.startsWith("-") {88 return Err("unknown option " + argument)89 } else {90 parsed = Parsed{..parsed, positional: parsed.positional.push(argument)}91 index = index + 192 }93 }94 let most = if parsed.command == "merge" { 1000 } else if parsed.command == "template" { 2 } else { 1 }95 if parsed.positional.length() > most { return Err("expected at most " + show(most) + " path" + (if most == 1 { "" } else { "s" }) + ", found " + parsed.positional.join(" ")) }96 Ok(parsed)97}9899100export fn usage() -> Str {101 var lines = ["Usage: docgen [command] [path] [options]", "", "Commands:"]102 for (name, description) in COMMANDS { lines = lines.push(" " + pad(name, 34) + description) }103 lines.concat(["", "Options:"]).concat(OPTIONS).join("\n")104}105106107export fn has(parsed: &Parsed, flag: Str) -> Bool { parsed.flags.contains(flag) }108109110export fn valueOf(parsed: &Parsed, name: Str, fallback: Str) -> Str {111 var found = fallback112 for (key, value) in parsed.values {113 if key == name { found = value }114 }115 found116}117118119export fn valuesOf(parsed: &Parsed, name: Str) -> Array[Str] {120 var found: Array[Str] = []121 for (key, value) in parsed.values {122 if key == name { found = found.push(value) }123 }124 found125}126127128export fn configPath(written: Str) -> Str {129 if written.endsWith(".json") || written.endsWith(".yml") || written.endsWith(".yaml") { written } else { Path.join(written, Package.CONFIG_FILE) }130}131132133fn longName(table: &Array[(Str, Str)], written: Str) -> Str {134 for (long, short) in *table {135 if written == long || (!short.isEmpty() && written == short) { return long }136 }137 ""138}139140141fn pad(text: Str, width: Int) -> Str {142 var pieces = [text]143 for _step in text.length()..width { pieces = pieces.push(" ") }144 pieces.join("") + " "145}146