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

Tasks.pudu

Pudu153 lines7.4 KB

GitHub ↗
1/** @Docgen.Docset.Tasks — project scaffolding, template export, printing, and reference maps */2module PuduLangDocgen.Docset.Tasks34import Std.Env as Env5import Std.Fs as Fs6import Std.Io as Io7import Std.List as List8import Std.Path as Path9import Std.Process as Process10import PuduLangDocgen.Build as Build11import PuduLangDocgen.Constants.Codes as Codes12import PuduLangDocgen.Constants.Package as Package13import PuduLangDocgen as Docgen14import PuduLangDocgen.Docset.Publish as Publish15import PuduLangDocgen.Docset.Tools as Tools16import PuduLangDocgen.Docset.Walk as Walk17import PuduLangDocgen.Paths as Paths18import PuduLangDocgen.References as References19import PuduLangDocgen.Scaffold as Scaffold20import PuduLangDocgen.Site.Print as Print21import PuduLangDocgen.Theme.Layout as Layout22import PuduLangDocgen.Theme as Theme2324/// Browsers tried, in order, when no renderer is configured.25const BROWSERS: Array[Str] = [26  "chromium", "chromium-browser", "google-chrome", "google-chrome-stable", "microsoft-edge", "msedge",27  "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Chromium.app/Contents/MacOS/Chromium",28  "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"29]3031/// Longest a renderer may take for one document unless `DOCGEN_PDF_TIMEOUT` says otherwise.32const PDF_MILLIS: Int = 1200003334/// A new project written into a folder; a folder that already holds a configuration is left alone.35export fn init(folder: Str, answers: &Scaffold.Answers) -> Result[Array[Str], Array[Docgen.Diagnostic]] {36  if Io.exists(Path.join(folder, Package.CONFIG_FILE)) { return Err([Docgen.error(Codes.CONFIG_INVALID, Path.join(folder, Package.CONFIG_FILE), 1, "a project already exists here")]) }37  var written: Array[Str] = []38  for (path, text) in Scaffold.files(answers) {39    let target = Path.join(folder, path)40    if Io.exists(target) { continue }41    write(target, text) ?42    written = written.push(path)43  }44  Ok(written)45}4647/// The default layout, partials, and assets written into a folder for customization.48export fn exportTemplate(folder: Str) -> Result[Array[Str], Array[Docgen.Diagnostic]] {49  var written: Array[Str] = []50  for (name, text) in Layout.templates() {51    let path = name + ".html"52    write(Path.join(folder, path), text) ?53    written = written.push(path)54  }55  for asset in Theme.assets() {56    let path = "reference/" + Path.nameOf(asset.path)57    write(Path.join(folder, path), asset.content) ?58    written = written.push(path)59  }60  write(Path.join(folder, "public/main.css"), "/* Styles loaded after the default site styles. */\n") ?61  write(Path.join(folder, "public/main.js"), "/* Behavior loaded after the default site script. */\n") ?62  Ok(written.push("public/main.css").push("public/main.js"))63}6465/// Printable documents for every table that sets `pdf`, published and then rendered to PDF.66export fn pdf(base: Str, built: &Build.Built, output: Str, renderer: &Array[Str]) -> Result[Array[Str], Array[Docgen.Diagnostic]] {67  let root = Path.join(base, output)68  let documents = Print.documents(&built.pages, &built.books)69  if documents.isEmpty() { return Err([Docgen.error(Codes.TOOL_FAILED, "toc.yml", 1, "no table of contents sets pdf: true")]) }70  let command = if renderer.isEmpty() { detect() } else { *renderer }71  if command.isEmpty() { return Err([Docgen.error(Codes.TOOL_FAILED, "pdf", 1, "no PDF renderer found; install Chromium or Chrome, or set pdf.renderer with \{input\} and \{output\}")]) }72  let limit = match Env.variable("DOCGEN_PDF_TIMEOUT") {73    case Some(written) => match written.toInt() { case Some(found) => found case None => PDF_MILLIS }74    case None => PDF_MILLIS75  }76  var produced: Array[Str] = []77  for document in documents {78    let input = Path.join(root, document.path)79    let target = Path.join(root, document.pdf)80    write(input, document.html) ?81    let header = Path.join(root, document.path + ".header.html")82    let footer = Path.join(root, document.path + ".footer.html")83    write(header, if document.header.isEmpty() { "<div></div>" } else { document.header }) ?84    write(footer, document.footer) ?85    let arguments = command.slice(1, command.length()).map(|part: Str| part.replace("\{input\}", input).replace("\{output\}", target).replace("\{header\}", header).replace("\{footer\}", footer).replace("\{url\}", "file://" + input))86    match Process.within(command[0], &arguments, limit) {87      case Ok(Some(finished)) => {88        if finished.status != 0 || !Io.exists(target) { return Err([Docgen.error(Codes.TOOL_FAILED, document.pdf, 1, "renderer failed: " + finished.errors.trim())]) }89      }90      case Ok(None) => { return Err([Docgen.error(Codes.TOOL_FAILED, document.pdf, 1, "renderer took longer than " + show(limit) + " ms")]) }91      case Err(reason) => { return Err([Docgen.error(Codes.TOOL_FAILED, document.pdf, 1, "renderer could not start: " + reason)]) }92    }93    let _cleaned = [Io.remove(header), Io.remove(footer)]94    produced = produced.push(document.pdf)95  }96  Ok(produced)97}9899/// A cross-reference map saved from a web address after checking that it reads.100export fn download(address: Str, file: Str) -> Result[Int, Array[Docgen.Diagnostic]] {101  let text = match Tools.fetch(address) {102    case Ok(found) => found103    case Err(reason) => { return Err([Docgen.error(Codes.XREF_UNAVAILABLE, address, 1, reason)]) }104  }105  let read = match References.parse(address, text) {106    case Ok(found) => found107    case Err(reason) => { return Err([Docgen.error(Codes.XREF_UNAVAILABLE, address, 1, reason)]) }108  }109  write(file, if file.toLower().endsWith(".json") { References.toJson(&read, "") } else { References.toYaml(&read, "") }) ?110  Ok(read.length())111}112113/// Cross-reference maps read from files or web addresses and written as one; the first map114/// to declare an identity keeps it.115export fn merge(sources: &Array[Str], file: Str) -> Result[Int, Array[Docgen.Diagnostic]] {116  var found: Array[Docgen.Reference] = []117  for source in *sources {118    let text = if Paths.remote(source) { Tools.fetch(source) } else { Io.read(source) }119    match text {120      case Ok(held) => match References.parse(source, held) {121        case Ok(read) => { found = found.concat(read) }122        case Err(reason) => { return Err([Docgen.error(Codes.XREF_UNAVAILABLE, source, 1, reason)]) }123      }124      case Err(reason) => { return Err([Docgen.error(Codes.XREF_UNAVAILABLE, source, 1, reason)]) }125    }126  }127  var kept: Array[Docgen.Reference] = []128  var seen: Array[Str] = []129  for reference in found {130    if !seen.contains(reference.uid) {131      kept = kept.push(reference)132      seen = seen.push(reference.uid)133    }134  }135  let ordered = List.sortOn(&kept, |held: Docgen.Reference| held.uid)136  write(file, if file.toLower().endsWith(".json") { References.toJson(&ordered, "") } else { References.toYaml(&ordered, "") }) ?137  Ok(ordered.length())138}139140/// A text file written in one step with its folder created.141fn write(path: Str, text: Str) -> Result[(), Array[Docgen.Diagnostic]] { Publish.place(path, text) }142143/// A renderer command for the first installed browser, or none.144fn detect() -> Array[Str] {145  for browser in BROWSERS {146    if Process.isAvailable(browser) || (browser.startsWith("/") && Io.exists(browser)) {147      return [browser, "--headless=new", "--disable-gpu", "--no-pdf-header-footer", "--print-to-pdf=\{output\}", "\{url\}"]148    }149  }150  if Process.isAvailable("wkhtmltopdf") { return ["wkhtmltopdf", "--enable-local-file-access", "--print-media-type", "--header-html", "\{header\}", "--footer-html", "\{footer\}", "\{input\}", "\{output\}"] }151  []152}153