
References.pudu
Pudu150 lines6.9 KB
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 Yaml131415export const UNRESOLVED: Str = "unresolved cross reference: "161718export const UID_SLOT: Str = "\{uid\}"192021const NOT_A_MAP: Str = "cross-reference map must be a mapping with references"22232425export 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}40414243export 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}565758export 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}7071727374export 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}112113114export 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}124125126127export fn serviceAddress(service: Str, uid: Str) -> Str {128 service.replace(UID_SLOT, Url.encodeComponent(uid))129}130131132export 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}135136137fn sorted(references: &Array[Docgen.Reference]) -> Array[Docgen.Reference] {138 List.sortOn(references, |held: Docgen.Reference| held.uid)139}140141142fn 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