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

Tools.pudu

Pudu134 lines6.1 KB

GitHub ↗
1/** @Docgen.Docset.Tools — git history, diagram rendering, remote reference maps, and reference services */2module PuduLangDocgen.Docset.Tools34import Std.Bytes as Bytes5import Std.Env as Env6import Std.Http.Client as Client7import Std.Process as Process8import Std.Result as Result9import PuduLangDocgen.Configuration as Configuration10import PuduLangDocgen.Constants.Codes as Codes11import PuduLangDocgen as Docgen12import PuduLangDocgen.Docset.Walk as Walk13import PuduLangDocgen.Paths as Paths14import PuduLangDocgen.References as References15import PuduLangDocgen.Repository as Repository1617/// Longest a remote reference map may take to arrive, in milliseconds.18const FETCH_MILLIS: Int = 300001920/// The latest change date of every file git tracks under a folder, or none without git.21export fn history(base: Str) -> Map[Str, Str] {22  if !Process.isAvailable("git") { return mapOf([]) }23  match Process.run("git", &["-C", base, "log", "--pretty=format:@%cs", "--name-only", "--relative", "--", "."]) {24    case Ok(finished) => if finished.status == 0 { Repository.history(finished.output) } else { mapOf([]) }25    case Err(_) => mapOf([])26  }27}2829/// The repository behind edit links: configured values first, then environment variables of30/// the running build service, then the local clone's `origin` remote and current branch.31export fn repository(base: Str, configured: &Configuration.Contribution) -> Configuration.Contribution {32  let (environmentRepo, environmentBranch) = Repository.fromEnvironment(&Env.variables())33  var repo = if configured.repo.isEmpty() { environmentRepo } else { Repository.normalize(configured.repo) }34  var branch = if environmentBranch.isEmpty() { configured.branch } else { environmentBranch }35  var prefix = configured.path36  if Process.isAvailable("git") {37    if repo.isEmpty() { repo = Repository.normalize(trimmed(Process.output("git", &["-C", base, "remote", "get-url", "origin"]))) }38    if environmentBranch.isEmpty() && configured.branch == "main" {39      let local = trimmed(Process.output("git", &["-C", base, "rev-parse", "--abbrev-ref", "HEAD"]))40      if !local.isEmpty() && local != "HEAD" { branch = local }41    }42    if prefix.isEmpty() {43      let found = trimmed(Process.output("git", &["-C", base, "rev-parse", "--show-prefix"]))44      prefix = if found.endsWith("/") { found.take(found.length() - 1) } else { found }45    }46  }47  Configuration.Contribution{repo: repo, branch: branch, path: prefix}48}4950/// PlantUML sources drawn by a local renderer as SVG data addresses; failures are reported51/// and leave the remote server to draw that diagram.52export fn diagrams(sources: &Array[Str], settings: &Configuration.Diagrams) -> (Map[Str, Str], Array[Docgen.Diagnostic]) {53  var drawn: Array[(Str, Str)] = []54  var problems: Array[Docgen.Diagnostic] = []55  for source in *sources {56    match Process.withInput(settings.java, &["-jar", settings.jar, "-tsvg", "-pipe"], source) {57      case Ok(finished) => {58        if finished.status == 0 && finished.output.contains("<svg") {59          drawn = drawn.push((source, "data:image/svg+xml;base64," + Bytes.encodeBase64(&Bytes.fromText(finished.output))))60        } else { problems = problems.push(Docgen.warning(Codes.TOOL_FAILED, settings.jar, 1, "PlantUML did not draw a diagram: " + finished.errors.trim())) }61      }62      case Err(reason) => { problems = problems.push(Docgen.warning(Codes.TOOL_FAILED, settings.jar, 1, "PlantUML could not start: " + reason)) }63    }64  }65  (mapOf(drawn), problems)66}6768/// References from cross-reference maps named by web address or project path.69export fn references(base: Str, sources: &Array[Str]) -> (Array[Docgen.Reference], Array[Docgen.Diagnostic]) {70  var found: Array[Docgen.Reference] = []71  var problems: Array[Docgen.Diagnostic] = []72  for source in *sources {73    let text = if Paths.remote(source) { fetch(source) } else { Result.mapErr(Walk.text(base, source), |problem: Docgen.Diagnostic| problem.message) }74    match text {75      case Ok(held) => match References.parse(source, held) {76        case Ok(read) => { found = found.concat(read) }77        case Err(reason) => { problems = problems.push(Docgen.warning(Codes.XREF_UNAVAILABLE, source, 1, reason)) }78      }79      case Err(reason) => { problems = problems.push(Docgen.warning(Codes.XREF_UNAVAILABLE, source, 1, reason)) }80    }81  }82  (found, problems)83}8485/// References that reference services know for identities a build left unresolved. Services86/// are asked in order; the first to answer for an identity is kept, and a service that87/// fails is reported once and not asked again.88export fn consult(services: &Array[Str], wanted: &Array[Str]) -> (Array[Docgen.Reference], Array[Docgen.Diagnostic]) {89  var found: Array[Docgen.Reference] = []90  var problems: Array[Docgen.Diagnostic] = []91  var failing: Array[Str] = []92  for uid in *wanted {93    for service in *services {94      if failing.contains(service) { continue }95      let address = References.serviceAddress(service, uid)96      let answered = match fetch(address) {97        case Ok(text) => References.parse(address, text)98        case Err(reason) => Err(reason)99      }100      match answered {101        case Ok(read) => {102          let matching = read.filter(|reference: Docgen.Reference| reference.uid == uid)103          if !matching.isEmpty() {104            found = found.push(matching[0])105            break106          }107        }108        case Err(reason) => {109          failing = failing.push(service)110          problems = problems.push(Docgen.warning(Codes.XREF_UNAVAILABLE, service, 1, reason))111        }112      }113    }114  }115  (found, problems)116}117118/// The body of a web address fetched within the deadline.119export fn fetch(address: Str) -> Result[Str, Str] {120  let limits = Client.Limits{..Client.limits(), deadlineMillis: FETCH_MILLIS}121  match Client.fetch(address, &limits) {122    case Ok(response) => if response.status.code >= 200 && response.status.code < 300 { Ok(response.body) } else { Err("server answered " + show(response.status.code)) }123    case Err(problem) => Err(Client.explain(&problem))124  }125}126127/// A command's output without surrounding white space, or the empty text when it failed.128fn trimmed(result: Result[Str, Str]) -> Str {129  match result {130    case Ok(held) => held.trim()131    case Err(_) => ""132  }133}134