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

Checks.pudu

Pudu81 lines3.4 KB

GitHub ↗
1/** @Docgen.Build.Checks — whole-site validation of fragments, outputs, and diagnostic levels */2module PuduLangDocgen.Build.Checks34import Std.Map as Map5import PuduLangDocgen.Constants.Codes as Codes6import PuduLangDocgen as Docgen7import PuduLangDocgen.Paths as Paths89/// Fragment links that name no element of their target page. Links to files other than pages10/// are not checked for fragments; links to missing files were reported while rendering.11export fn fragments(pages: &Array[Docgen.Page]) -> Array[Docgen.Diagnostic] {12  var ids: Map[Str, Array[Str]] = mapOf([])13  for page in *pages { ids = Map.insert(&ids, page.path, anchors(page.body)) }14  var problems: Array[Docgen.Diagnostic] = []15  for page in *pages {16    for link in page.links {17      let parts = link.target.split("#")18      if parts.length() < 2 || parts[1].isEmpty() { continue }19      match Map.get(&ids, parts[0]) {20        case Some(found) => {21          if !found.contains(parts[1]) { problems = problems.push(Docgen.warning(Codes.FRAGMENT_MISSING, page.source, link.line, "no section " + parts[1] + " in " + parts[0])) }22        }23        case None => {}24      }25    }26  }27  problems28}2930/// Every `id` attribute value in HTML.31export fn anchors(html: Str) -> Array[Str] {32  var result: Array[Str] = []33  var rest = html34  var at = rest.indexOf(" id=\"")35  while at >= 0 {36    rest = rest.drop(at + 5)37    let end = rest.indexOf("\"")38    if end < 0 { break }39    result = result.push(rest.take(end).replace("&amp;", "&").replace("&quot;", "\"").replace("&#39;", "'").replace("&lt;", "<").replace("&gt;", ">"))40    rest = rest.drop(end)41    at = rest.indexOf(" id=\"")42  }43  result44}4546/// Output paths that are not portable, collide ignoring case, or put a file where a folder is.47export fn outputs(paths: &Array[Str]) -> Array[Docgen.Diagnostic] {48  var problems: Array[Docgen.Diagnostic] = []49  var seen: Map[Str, Bool] = mapOf([])50  var folders: Map[Str, Bool] = mapOf([])51  for path in *paths {52    let lower = path.toLower()53    if !Paths.relative(path) { problems = problems.push(Docgen.error(Codes.OUTPUT_INVALID, path, 1, "output path is not portable")) }54    if Map.containsKey(&seen, lower) { problems = problems.push(Docgen.error(Codes.OUTPUT_COLLISION, path, 1, "two outputs share this path")) }55    seen = Map.insert(&seen, lower, true)56    let parts = lower.split("/")57    for depth in 1..parts.length() { folders = Map.insert(&folders, parts.slice(0, depth).join("/"), true) }58  }59  for path in *paths {60    if Map.containsKey(&folders, path.toLower()) { problems = problems.push(Docgen.error(Codes.OUTPUT_COLLISION, path, 1, "an output file has the path of an output folder")) }61  }62  problems63}6465/// Diagnostics with configured levels applied: `off` drops a code, `info`, `warning`, and66/// `error` set its severity, and with `strict` every remaining warning becomes an error.67export fn ruled(diagnostics: &Array[Docgen.Diagnostic], rules: &Array[(Str, Str)], strict: Bool) -> Array[Docgen.Diagnostic] {68  var result: Array[Docgen.Diagnostic] = []69  for held in *diagnostics {70    var level = ""71    for (code, chosen) in *rules {72      if code == held.code { level = chosen }73    }74    if level == "off" { continue }75    var severity = if level == "error" { Docgen.Error } else if level == "warning" { Docgen.Warning } else if level == "info" { Docgen.Information } else { held.severity }76    if strict && severity == Docgen.Warning { severity = Docgen.Error }77    result = result.push(Docgen.Diagnostic{..held, severity: severity})78  }79  result80}81