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

Actions.pudu

Pudu177 lines7.5 KB

GitHub ↗
1/** @Docgen.Command.Actions — reporting and the commands that create and exchange files */2module PuduLangDocgen.Command.Actions34import Std.Io as Io5import Std.Path as Path6import PuduLangDocgen.Command.Arguments as Arguments7import PuduLangDocgen.Constants.Package as Package8import PuduLangDocgen as Docgen9import PuduLangDocgen.Docset.Tasks as Tasks10import PuduLangDocgen.Scaffold as Scaffold11import PuduLangLog.Configuration as LogConfiguration12import PuduLangLog.Formatting.Compact as Compact13import PuduLangLog as Log14import PuduLangLog.Logger as Logger15import PuduLangLog.Sinks.Console as Console16import PuduLangLog.Sinks.File as File17import PuduLangLog.Sinks.Theme as Theme18import PuduLangLog.Value as Value1920/// Console layout: the rendered message alone, one per line.21const CONSOLE_TEMPLATE: Str = "\{Message:lj\}\{NewLine\}"2223/// Template of every diagnostic event; its properties keep the code and location as data.24const DIAGNOSTIC_TEMPLATE: Str = "\{Path\}:\{Line\}: \{Severity\} \{Code\}: \{Text\}"2526/// A logger for a command line: messages at or above `--logLevel` (information unless set,27/// debug with `--verbose`) on the console with warnings and errors on standard error, and every28/// event as compact JSON in the `--log` file when one is named.29export fn logger(parsed: &Arguments.Parsed) -> Result[Logger.Logger, Str] {30  let written = Arguments.valueOf(parsed, "--logLevel", if Arguments.has(parsed, "--verbose") { "verbose" } else { "info" })31  let minimum = match levelOf(written) {32    case Some(found) => found33    case None => { return Err("log level must be error, warning, info, verbose, or diagnostic: " + written) }34  }35  var configured = LogConfiguration.create().minimumLevel(minimum).writeTo(Console.sink(Console.Options{..Console.defaults(), outputTemplate: CONSOLE_TEMPLATE, theme: Theme.none(), colored: false, standardErrorFromLevel: Some(Log.Warning)}))36  let file = Arguments.valueOf(parsed, "--log", "")37  if !file.isEmpty() { configured = configured.writeTo(File.sink(File.Options{..File.defaults(file), formatter: Compact.compact()})) }38  Ok(configured.createLogger())39}4041/// Diagnostics written as events at their severity.42export fn report(log: &Logger.Logger, problems: &Array[Docgen.Diagnostic]) -> () {43  for problem in *problems {44    let values = [Value.text(problem.path), Value.int(problem.line), Value.text(severity(&problem)), Value.text(problem.code), Value.text(problem.message)]45    match problem.severity {46      case Docgen.Error => log.error(DIAGNOSTIC_TEMPLATE, values)47      case Docgen.Warning => log.warning(DIAGNOSTIC_TEMPLATE, values)48      case Docgen.Information => log.information(DIAGNOSTIC_TEMPLATE, values)49    }50  }51}5253/// A plain message at information level.54export fn say(log: &Logger.Logger, text: Str) -> () { log.information("\{Text\}", [Value.text(text)]) }5556/// A plain message at debug level, shown with `--verbose`.57export fn detail(log: &Logger.Logger, text: Str) -> () { log.debug("\{Text\}", [Value.text(text)]) }5859/// Reports failures and answers the failed-work status.60export fn failed(log: &Logger.Logger, problems: &Array[Docgen.Diagnostic]) -> Int {61  report(log, problems)62  let errors = problems.filter(|problem: Docgen.Diagnostic| problem.severity == Docgen.Error).length()63  log.error("Failed with \{Errors\} error(s) and \{Warnings\} warning(s).", [Value.int(errors), Value.int(problems.length() - errors)])64  165}6667/// Creates a project, asking for a title and API sources unless `--yes` accepts defaults.68export fn initialize(log: &Logger.Logger, parsed: &Arguments.Parsed) -> Int {69  let folder = if parsed.positional.isEmpty() { "." } else { parsed.positional[0] }70  let name = Path.nameOf(if folder == "." { Path.directoryOf(Path.join(folder, "x")) } else { folder })71  var answers = Scaffold.defaults(name)72  if !Arguments.has(parsed, "--yes") {73    answers = Scaffold.Answers{..answers, title: ask("Site title", answers.title)}74    answers = Scaffold.Answers{..answers, api: ask("Document Pudu sources (yes/no)", "yes").toLower().startsWith("y")}75    if answers.api { answers = Scaffold.Answers{..answers, sources: ask("Source folder", answers.sources)} }76    answers = Scaffold.Answers{..answers, pdf: ask("Enable PDF (yes/no)", "no").toLower().startsWith("y")}77  }78  match Tasks.init(folder, &answers) {79    case Ok(written) => {80      for path in written { say(log, "created " + path) }81      say(log, "Run: docgen " + (if folder == "." { "" } else { folder + " " }) + "--serve")82      083    }84    case Err(problems) => failed(log, &problems)85  }86}8788/// Lists built-in templates or exports one, or every one with `--all`, into a folder.89export fn template(log: &Logger.Logger, parsed: &Arguments.Parsed) -> Int {90  let action = if parsed.positional.isEmpty() { "list" } else { parsed.positional[0] }91  if action == "list" {92    for name in Package.BUILT_IN_TEMPLATES { say(log, name) }93    return 094  }95  if action != "export" {96    log.error("template expects list or export", [])97    return 298  }99  let chosen = if parsed.positional.length() > 1 { parsed.positional[1] } else { "default" }100  let names = if Arguments.has(parsed, "--all") { ["default"] } else { [chosen] }101  let root = Arguments.valueOf(parsed, "--output", "_exported_templates")102  for name in names {103    if name != "default" && name != "modern" && name != "statictoc" {104      log.error("template \{Name\} has no files to export", [Value.text(name)])105      return 2106    }107    match Tasks.exportTemplate(Path.join(root, name)) {108      case Ok(written) => { for path in written { detail(log, "wrote " + Path.join(Path.join(root, name), path)) } }109      case Err(problems) => { return failed(log, &problems) }110    }111    say(log, "exported " + name + " to " + Path.join(root, name))112  }113  0114}115116/// Saves a cross-reference map named by `--xref` into a file.117export fn download(log: &Logger.Logger, parsed: &Arguments.Parsed) -> Int {118  let address = Arguments.valueOf(parsed, "--xref", "")119  if parsed.positional.isEmpty() || address.isEmpty() {120    log.error("download needs a file and --xref <address>", [])121    return 2122  }123  match Tasks.download(address, parsed.positional[0]) {124    case Ok(count) => {125      say(log, "saved " + show(count) + " references to " + parsed.positional[0])126      0127    }128    case Err(problems) => failed(log, &problems)129  }130}131132/// Combines cross-reference maps into the `--output` file.133export fn merge(log: &Logger.Logger, parsed: &Arguments.Parsed) -> Int {134  let file = Arguments.valueOf(parsed, "--output", "")135  if parsed.positional.isEmpty() || file.isEmpty() {136    log.error("merge needs maps and --output <file>", [])137    return 2138  }139  match Tasks.merge(&parsed.positional, file) {140    case Ok(count) => {141      say(log, "merged " + show(count) + " references into " + file)142      0143    }144    case Err(problems) => failed(log, &problems)145  }146}147148/// The minimum level a `--logLevel` name sets.149fn levelOf(name: Str) -> Option[Log.Level] {150  match name {151    case "error" => Some(Log.Error)152    case "warning" => Some(Log.Warning)153    case "info" => Some(Log.Information)154    case "verbose" => Some(Log.Debug)155    case "diagnostic" => Some(Log.Verbose)156    case _ => None157  }158}159160/// The word a severity is shown with.161fn severity(problem: &Docgen.Diagnostic) -> Str {162  match problem.severity {163    case Docgen.Error => "error"164    case Docgen.Warning => "warning"165    case Docgen.Information => "info"166  }167}168169/// An answer read from the console, or the default for an empty line.170fn ask(question: Str, fallback: Str) -> Str {171  let _asked = Io.writeLine(question + " [" + fallback + "]: ")172  match Io.readLineOrEnd() {173    case Ok(Some(line)) => if line.trim().isEmpty() { fallback } else { line.trim() }174    case _ => fallback175  }176}177