Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-docgen

Documentation publishing for Pudu: articles, API references, navigation, search, and static websites

0.1.0Apache-2.01

InstallClose

Command.pudu

Pudu197 lines7.9 KB

GitHub ↗
1/** @Docgen.Command.Seam — the docgen command line and its exit statuses */2module PuduLangDocgen.Command34import Std.Concurrent as Concurrent5import Std.Io as Io6import Std.Path as Path7import Std.Process as Process8import PuduLangDocgen.Build as Build9import PuduLangDocgen.Command.Actions as Actions10import PuduLangDocgen.Command.Arguments as Arguments11import PuduLangDocgen.Constants.Package as Package12import PuduLangDocgen as Docgen13import PuduLangDocgen.Docset as Docset14import PuduLangDocgen.Serve as Serve15import PuduLangLog.Logger as Logger16import PuduLangLog.Value as Value1718/// Default port of the preview server.19const PORT: Int = 80802021/// Milliseconds between checks for changed sources while watching.22const WATCH_MILLIS: Int = 10002324/// Milliseconds to wait for the preview server before opening a browser.25const OPEN_DELAY_MILLIS: Int = 6002627/// Programs that open an address in the default browser, tried in order.28const OPENERS: Array[Str] = ["open", "xdg-open", "wslview"]2930/// Runs a command line and answers its exit status: 0 success, 1 failed work, 2 invalid usage.31export fn run(arguments: &Array[Str]) -> Int {32  let parsed = match Arguments.parse(arguments) {33    case Ok(found) => found34    case Err(reason) => {35      let _said = Io.writeErrorLine(reason + "\n\n" + Arguments.usage())36      return 237    }38  }39  if Arguments.has(&parsed, "--help") || parsed.command == "help" {40    let _said = Io.writeLine(Arguments.usage())41    return 042  }43  if parsed.command == "version" {44    let _said = Io.writeLine(Package.NAME + " " + Package.VERSION)45    return 046  }47  let log = match Actions.logger(&parsed) {48    case Ok(found) => found49    case Err(reason) => {50      let _said = Io.writeErrorLine(reason)51      return 252    }53  }54  let status = dispatch(&log, &parsed)55  Logger.close(&log)56  status57}5859/// The command a parsed line names, run with a logger.60fn dispatch(log: &Logger.Logger, parsed: &Arguments.Parsed) -> Int {61  if parsed.command == "serve" { return serve(log, if parsed.positional.isEmpty() { "_site" } else { parsed.positional[0] }, parsed, "") }62  if parsed.command == "init" { return Actions.initialize(log, parsed) }63  if parsed.command == "template" { return Actions.template(log, parsed) }64  if parsed.command == "download" { return Actions.download(log, parsed) }65  if parsed.command == "merge" { return Actions.merge(log, parsed) }66  let config = if parsed.positional.isEmpty() { Package.CONFIG_FILE } else { Arguments.configPath(parsed.positional[0]) }67  let chosen = match options(parsed) {68    case Ok(found) => found69    case Err(reason) => {70      log.error("\{Reason\}", [Value.text(reason)])71      return 272    }73  }74  if parsed.command == "metadata" {75    return match Docset.metadata(config, &chosen) {76      case Ok(written) => {77        for path in written { Actions.say(log, "wrote " + path) }78        079      }80      case Err(problems) => Actions.failed(log, &problems)81    }82  }83  if parsed.command == "pdf" {84    return match Docset.pdf(config, &chosen) {85      case Ok(produced) => {86        for path in produced { Actions.say(log, "wrote " + path) }87        088      }89      case Err(problems) => Actions.failed(log, &problems)90    }91  }92  let outcome = if parsed.command == "build" {93    match Docset.build(config, &chosen, &Build.extensions()) {94      case Ok(report) => Ok((report, []))95      case Err(problems) => Err(problems)96    }97  } else { Docset.buildAll(config, &chosen) }98  match outcome {99    case Ok((report, produced)) => {100      Actions.report(log, &report.diagnostics)101      for path in report.written { Actions.detail(log, "wrote " + path) }102      for path in report.removed { Actions.detail(log, "removed " + path) }103      for path in produced { Actions.say(log, "wrote " + path) }104      Actions.say(log, "Build succeeded: " + show(report.written.length()) + " written, " + show(report.unchanged.length()) + " unchanged, " + show(report.removed.length()) + " removed.")105      if Arguments.has(parsed, "--serve") {106        let folder = Path.join(Path.directoryOf(config), if chosen.output.isEmpty() { "_site" } else { chosen.output })107        return serve(log, folder, parsed, config)108      }109      0110    }111    case Err(problems) => Actions.failed(log, &problems)112  }113}114115/// Docset options from parsed arguments.116fn options(parsed: &Arguments.Parsed) -> Result[Docset.Options, Str] {117  var metadata: Array[(Str, Docgen.Meta)] = []118  for written in Arguments.valuesOf(parsed, "--metadata") {119    let at = written.indexOf("=")120    if at < 1 { return Err("metadata must be written key=value: " + written) }121    let value = written.drop(at + 1)122    metadata = metadata.push((written.take(at), if value == "true" { Docgen.Flag(true) } else if value == "false" { Docgen.Flag(false) } else { Docgen.Text(value) }))123  }124  Ok(Docset.Options{..Docset.options(),125      output: Arguments.valueOf(parsed, "--output", ""), metadata: metadata, xref: Arguments.valuesOf(parsed, "--xref"),126      templates: Arguments.valuesOf(parsed, "--template").concat(Arguments.valuesOf(parsed, "--theme")),127      force: Arguments.has(parsed, "--force"), dryRun: Arguments.has(parsed, "--dryRun"), warningsAsErrors: Arguments.has(parsed, "--warningsAsErrors"),128      disableGitFeatures: Arguments.has(parsed, "--disableGitFeatures"), exportRawModel: Arguments.has(parsed, "--exportRawModel"),129      exportViewModel: Arguments.has(parsed, "--exportViewModel") })130}131132/// Serves a folder with the host and port options, answering an exit status. With `--watch`133/// and a configuration, the site is rebuilt whenever a source changes; `--open-browser` and134/// `--open-file` open the preview once the server is listening.135fn serve(log: &Logger.Logger, folder: Str, parsed: &Arguments.Parsed, config: Str) -> Int {136  let host = Arguments.valueOf(parsed, "--hostname", "localhost")137  let port = match Arguments.valueOf(parsed, "--port", show(PORT)).toInt() {138    case Some(found) => found139    case None => {140      log.error("port must be a number", [])141      return 2142    }143  }144  if !config.isEmpty() && Arguments.has(parsed, "--watch") { watch(log, config, parsed) }145  let file = Arguments.valueOf(parsed, "--open-file", "")146  if Arguments.has(parsed, "--open-browser") || !file.isEmpty() {147    let address = "http://" + (if host == "0.0.0.0" { "localhost" } else { host }) + ":" + show(port) + "/" + file148    let _opening = Concurrent.start(fn() -> () {149        let _paused = Concurrent.sleep(OPEN_DELAY_MILLIS)150        for opener in OPENERS {151          if Process.isAvailable(opener) {152            let _opened = Process.run(opener, &[address])153            return154          }155        }156      })157  }158  match Serve.serve(folder, host, port) {159    case Ok(_) => 0160    case Err(reason) => {161      log.error("cannot serve \{Folder\}: \{Reason\}", [Value.text(folder), Value.text(reason)])162      1163    }164  }165}166167/// Starts rebuilding the site whenever a source's content changes.168fn watch(log: &Logger.Logger, config: Str, parsed: &Arguments.Parsed) -> () {169  let chosen = match options(parsed) {170    case Ok(found) => found171    case Err(_) => Docset.options()172  }173  let output = if chosen.output.isEmpty() { "_site" } else { chosen.output }174  let reporter = *log175  let started = Concurrent.start(fn() -> () {176      var seen = Docset.fingerprint(config, output)177      while true {178        let _paused = Concurrent.sleep(WATCH_MILLIS)179        let current = Docset.fingerprint(config, output)180        if current != seen {181          seen = current182          match Docset.build(config, &chosen, &Build.extensions()) {183            case Ok(report) => {184              Actions.report(&reporter, &report.diagnostics)185              Actions.say(&reporter, "Rebuilt: " + show(report.written.length()) + " written, " + show(report.removed.length()) + " removed.")186            }187            case Err(problems) => Actions.report(&reporter, &problems)188          }189        }190      }191    })192  match started {193    case Ok(_) => Actions.say(log, "Watching " + config + " for changes")194    case Err(_) => log.warning("cannot watch for changes; serving without rebuilding", [])195  }196}197