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

References.pudu

Pudu150 lines6.9 KB

GitHub ↗
1/** @Docgen.References.Module — unique identities and exchanged cross-reference maps */2module PuduLangDocgen.References34import Std.Json as Json5import Std.List as List6import Std.Map as Map7import Std.Url as Url8import PuduLangDocgen.Constants.Codes as Codes9import PuduLangDocgen as Docgen10import PuduLangDocgen.Meta as Meta11import PuduLangDocgen.Paths as Paths12import PuduLangDocgen.Yaml as Yaml1314/// Message prefix of an unresolved cross reference, followed by its identity.15export const UNRESOLVED: Str = "unresolved cross reference: "1617/// Placeholder a reference service address holds for the identity asked about.18export const UID_SLOT: Str = "\{uid\}"1920/// Refusal of a document that is neither a map nor a service's list of references.21const NOT_A_MAP: Str = "cross-reference map must be a mapping with references"2223/// References by identity: page identities first, then external maps. A page identity24/// declared twice is an error; an external identity already known is skipped.25export fn registry(own: &Array[Docgen.Reference], external: &Array[Docgen.Reference]) -> (Map[Str, Docgen.Reference], Array[Docgen.Diagnostic]) {26  var found: Map[Str, Docgen.Reference] = mapOf([])27  var problems: Array[Docgen.Diagnostic] = []28  for reference in *own {29    if reference.uid.isEmpty() { continue }30    match Map.get(&found, reference.uid) {31      case Some(earlier) => { problems = problems.push(Docgen.error(Codes.UID_DUPLICATE, reference.href, 1, "uid " + reference.uid + " is also declared by " + earlier.href)) }32      case None => { found = Map.insert(&found, reference.uid, reference) }33    }34  }35  for reference in *external {36    if !reference.uid.isEmpty() && !Map.containsKey(&found, reference.uid) { found = Map.insert(&found, reference.uid, reference) }37  }38  (found, problems)39}4041/// A cross-reference map as YAML, sorted by identity. Relative destinations are relative to42/// the site root, which `baseUrl` names when it is not empty.43export fn toYaml(references: &Array[Docgen.Reference], baseUrl: Str) -> Str {44  var lines = ["### YamlMime:XRefMap", "sorted: true"]45  if !baseUrl.isEmpty() { lines = lines.push("baseUrl: " + Yaml.scalar(baseUrl)) }46  lines = lines.push("references:")47  for reference in sorted(references) {48    lines = lines.push("- uid: " + Yaml.scalar(reference.uid))49    lines = lines.push("  name: " + Yaml.scalar(reference.name))50    if !reference.fullName.isEmpty() { lines = lines.push("  fullName: " + Yaml.scalar(reference.fullName)) }51    lines = lines.push("  href: " + Yaml.scalar(reference.href))52    if !reference.kind.isEmpty() { lines = lines.push("  type: " + Yaml.scalar(reference.kind)) }53  }54  lines.join("\n") + "\n"55}5657/// A cross-reference map as JSON with the same fields as the YAML form.58export fn toJson(references: &Array[Docgen.Reference], baseUrl: Str) -> Str {59  let entries = sorted(references).map(fn(reference: Docgen.Reference) -> Json.Json {60      var fields = [("uid", Json.Text(reference.uid)), ("name", Json.Text(reference.name))]61      if !reference.fullName.isEmpty() { fields = fields.push(("fullName", Json.Text(reference.fullName))) }62      fields = fields.push(("href", Json.Text(reference.href)))63      if !reference.kind.isEmpty() { fields = fields.push(("type", Json.Text(reference.kind))) }64      Json.object(&fields)65    })66  var top = [("sorted", Json.Boolean(true))]67  if !baseUrl.isEmpty() { top = top.push(("baseUrl", Json.Text(baseUrl))) }68  Json.encodePretty(&Json.object(&top.push(("references", Json.list(&entries)))))69}7071/// References read from a YAML or JSON cross-reference map, or from the bare JSON list of72/// references a reference service answers with. Relative destinations resolve against the73/// map's `baseUrl`, or else against the address the map was read from.74export fn parse(origin: Str, text: Str) -> Result[Array[Docgen.Reference], Str] {75  let trimmed = text.trim()76  let value = if trimmed.startsWith("\{") || trimmed.startsWith("[") {77    match Json.decode(trimmed) {78      case Ok(found) => Meta.fromJson(&found)79      case Err(problem) => { return Err("cross-reference map is not valid JSON: " + Json.explain(&problem)) }80    }81  } else { Yaml.decode(text) ? }82  let fields = match value {83    case Docgen.Fields(found) => found84    case Docgen.Items(found) => {85      if !trimmed.startsWith("[") { return Err(NOT_A_MAP) }86      [("references", Docgen.Items(found))]87    }88    case _ => { return Err(NOT_A_MAP) }89  }90  let base = Meta.textOr(&fields, "baseUrl", "")91  let listed = match Meta.get(&fields, "references") {92    case Some(Docgen.Items(found)) => found93    case Some(Docgen.Nothing) => []94    case None => []95    case _ => { return Err("references must be a list") }96  }97  var result: Array[Docgen.Reference] = []98  for held in listed {99    let entry = match held {100      case Docgen.Fields(found) => found101      case _ => { return Err("each reference must be a mapping") }102    }103    let uid = Meta.textOr(&entry, "uid", "")104    let href = Meta.textOr(&entry, "href", "")105    if uid.isEmpty() { return Err("a reference has no uid") }106    if !href.isEmpty() && !Paths.href(href) { return Err("reference " + uid + " has an unsafe href") }107    let located = if href.isEmpty() || Paths.remote(href) { href } else { absolute(base, origin, href) }108    result = result.push(Docgen.Reference{uid: uid, name: Meta.textOr(&entry, "name", uid), fullName: Meta.textOr(&entry, "fullName", ""), href: located, kind: Meta.textOr(&entry, "type", "")})109  }110  Ok(result)111}112113/// Identities of the cross references a build reported as unresolved, each once.114export fn unresolved(diagnostics: &Array[Docgen.Diagnostic]) -> Array[Str] {115  var found: Array[Str] = []116  for held in *diagnostics {117    if held.code == Codes.XREF_UNRESOLVED && held.message.startsWith(UNRESOLVED) {118      let uid = held.message.drop(UNRESOLVED.length())119      if !found.contains(uid) { found = found.push(uid) }120    }121  }122  found123}124125/// The address a reference service is asked at for one identity: every `\{uid\}` in the126/// service address replaced by the percent-encoded identity.127export fn serviceAddress(service: Str, uid: Str) -> Str {128  service.replace(UID_SLOT, Url.encodeComponent(uid))129}130131/// References of published pages that declare an identity.132export fn ofPages(pages: &Array[Docgen.Page]) -> Array[Docgen.Reference] {133  pages.filter(|page: Docgen.Page| !page.uid.isEmpty()).map(|page: Docgen.Page| Docgen.Reference{uid: page.uid, name: page.title, fullName: page.title, href: page.path, kind: page.kind})134}135136/// References ordered by identity.137fn sorted(references: &Array[Docgen.Reference]) -> Array[Docgen.Reference] {138  List.sortOn(references, |held: Docgen.Reference| held.uid)139}140141/// A destination joined to a base address, or to the folder of the address a map came from.142fn absolute(base: Str, origin: Str, href: Str) -> Str {143  if !base.isEmpty() { return if base.endsWith("/") { base + href } else { base + "/" + href } }144  if Paths.remote(origin) {145    let cut = origin.split("/")146    return cut.slice(0, cut.length() - 1).join("/") + "/" + href147  }148  href149}150