
Pages.pudu
Pudu243 lines12.9 KB
1/** @Docgen.Api.Pages — reference pages for modules and the types they declare */2module PuduLangDocgen.Api.Pages34import Std.Map as Map5import PuduLangDocgen.Api.Catalog as Catalog6import PuduLangDocgen.Api.Model as Model7import PuduLangDocgen.Api.Signature as Signature8import PuduLangDocgen as Docgen9import PuduLangDocgen.Markdown as Markdown10import PuduLangDocgen.Markdown.Phrase as Phrase11import PuduLangDocgen.Paths as Paths1213/** @Docgen.Api.Context — everything reference pages link against */14export type Context = { options: Catalog.Options, references: Map[Str, Docgen.Reference], outputs: Map[Str, Str], settings: Phrase.Rendering, units: Array[Model.Unit] }1516/** @Docgen.Api.Body — page HTML with the sections, links, and findings gathered on the way */17type Body = { pieces: Array[Str], headings: Array[Docgen.Heading], links: Array[Docgen.Link], diagnostics: Array[Docgen.Diagnostic] }181920const SECTIONS: Array[(Str, Str, Array[Str])] = [21 ("types", "Types", ["record", "union", "alias", "opaque"]), ("traits", "Traits", ["trait"]),22 ("functions", "Functions", ["function"]), ("constants", "Constants", ["constant"])23]242526const KIND_NAMES: Array[(Str, Str)] = [27 ("record", "Record"), ("union", "Union"), ("alias", "Type alias"), ("opaque", "Opaque type"), ("trait", "Trait"),28 ("function", "Function"), ("constant", "Constant"), ("method", "Method")29]303132export fn pages(unit: &Model.Unit, context: &Context) -> (Array[Docgen.Page], Array[Docgen.Diagnostic]) {33 let (first, found) = modulePage(unit, context)34 var result = [first]35 var problems = found36 for member in unit.members {37 if Model.isType(member.kind) || context.options.separate {38 let (page, more) = if Model.isType(member.kind) { typePage(unit, &member, context) } else { memberPage(unit, &member, context) }39 result = result.push(page)40 problems = problems.concat(more)41 }42 }43 (result, problems)44}454647export fn kindName(kind: Str) -> Str {48 for (key, name) in KIND_NAMES {49 if key == kind { return name }50 }51 kind52}535455fn start() -> Body { Body{pieces: [], headings: [], links: [], diagnostics: []} }565758fn emit(body: Body, text: Str) -> Body { Body{..body, pieces: body.pieces.push(text)} }596061fn section(body: Body, level: Int, id: Str, title: Str) -> Body {62 let tag = "h" + show(level)63 let recorded = Body{..body, headings: body.headings.push(Docgen.Heading{id: id, title: title, level: level})}64 emit(recorded, "<" + tag + " id=\"" + Paths.escape(id) + "\">" + Paths.escape(title) + "<a class=\"anchor\" href=\"#" + Paths.escape(id) + "\" aria-label=\"Link to this section\"></a></" + tag + ">\n")65}666768fn documentation(body: Body, text: Str, unit: &Model.Unit, page: Str, line: Int, context: &Context) -> Body {69 if text.trim().isEmpty() { return body }70 if context.options.skipMarkup {71 let paragraphs = text.split("\n\n").filter(|part: Str| !part.trim().isEmpty()).map(|part: Str| "<p>" + Paths.escape(part.trim()) + "</p>")72 return emit(body, "<div class=\"api-doc\">\n" + paragraphs.join("\n") + "\n</div>\n")73 }74 let located = Phrase.Scope{page: page, source: unit.path, outputs: context.outputs, references: context.references, settings: context.settings}75 let rendered = Markdown.fragment(text, &located, 2)76 let found = rendered.diagnostics.map(|held: Docgen.Diagnostic| Docgen.Diagnostic{..held, line: line})77 let recorded = Body{..body, links: body.links.concat(rendered.links), diagnostics: body.diagnostics.concat(found)}78 emit(recorded, "<div class=\"api-doc\">\n" + rendered.html + "</div>\n")79}808182fn source(body: Body, unit: &Model.Unit, line: Int, context: &Context) -> Body {83 let href = Catalog.sourceLink(&context.options, unit.path, line)84 if href.isEmpty() { body } else { emit(body, "<p class=\"api-source\"><a class=\"external\" href=\"" + Paths.escape(href) + "\">View source</a></p>\n") }85}868788fn declaration(body: Body, unit: &Model.Unit, signature: Str, page: Str, context: &Context) -> Body {89 emit(body, "<div class=\"api-signature\"><pre><code class=\"lang-pudu\">" + Signature.html(unit, signature, &context.references, page) + "</code></pre></div>\n")90}919293fn heading(body: Body, kind: Str, title: Str) -> Body {94 emit(body, "<div class=\"api-heading\"><p class=\"api-kind\">" + Paths.escape(kind) + "</p><h1 id=\"" + Paths.escape(Paths.slug(title)) + "\">" + Paths.escape(title) + "</h1></div>\n")95}969798fn finish(body: Body, path: Str, unit: &Model.Unit, kind: Str, title: Str, uid: Str, summary: Str) -> (Docgen.Page, Array[Docgen.Diagnostic]) {99 let page = Docgen.Page{path: path, source: unit.path, kind: kind, title: title, uid: uid, summary: summary, body: body.pieces.join(""), headings: body.headings, links: body.links, meta: []}100 (page, body.diagnostics)101}102103104fn summaryTable(body: Body, members: &Array[Model.Member], page: Str, context: &Context) -> Body {105 var held = emit(body, "<div class=\"table-wrapper\"><table class=\"api-table\"><thead><tr><th>Name</th><th>Description</th></tr></thead><tbody>\n")106 for member in *members {107 let href = match Map.get(&context.references, member.uid) {108 case Some(found) => Paths.between(page, found.href)109 case None => "#" + member.name110 }111 held = emit(held, "<tr><td><a class=\"xref\" href=\"" + Paths.escape(href) + "\"><code>" + Paths.escape(member.name) + "</code></a></td><td>" + Paths.escape(Model.summary(member.doc)) + "</td></tr>\n")112 }113 emit(held, "</tbody></table></div>\n")114}115116117fn detail(body: Body, unit: &Model.Unit, member: &Model.Member, page: Str, context: &Context) -> Body {118 var held = emit(body, "<section class=\"api-member\">\n")119 held = section(held, 3, member.name, member.name)120 held = declaration(held, unit, member.signature, page, context)121 held = documentation(held, member.doc, unit, page, member.line, context)122 let shown = member.parameters.filter(|parameter: Model.Parameter| parameter.name != "self")123 if !shown.isEmpty() {124 held = emit(held, "<h4>Parameters</h4>\n<dl class=\"api-parameters\">\n")125 for parameter in shown {126 held = emit(held, "<dt><code>" + Paths.escape(parameter.name) + "</code></dt><dd><code>" + Signature.html(unit, parameter.kind, &context.references, page) + "</code></dd>\n")127 }128 held = emit(held, "</dl>\n")129 }130 if !member.returns.isEmpty() {131 let label = if member.kind == "constant" { "Type" } else { "Returns" }132 held = emit(held, "<h4>" + label + "</h4>\n<p><code>" + Signature.html(unit, member.returns, &context.references, page) + "</code></p>\n")133 }134 emit(source(held, unit, member.line, context), "</section>\n")135}136137138fn modulePage(unit: &Model.Unit, context: &Context) -> (Docgen.Page, Array[Docgen.Diagnostic]) {139 let page = Catalog.pageOf(&context.options, unit.uid)140 var body = heading(start(), "Module", unit.uid)141 body = source(body, unit, unit.line, context)142 body = documentation(body, unit.doc, unit, page, unit.line, context)143 for (id, title, kinds) in SECTIONS {144 let members = unit.members.filter(|member: Model.Member| kinds.contains(member.kind))145 if members.isEmpty() { continue }146 body = summaryTable(section(body, 2, id, title), &members, page, context)147 if (id == "functions" || id == "constants") && !context.options.separate {148 for member in members { body = detail(body, unit, &member, page, context) }149 }150 }151 finish(body, page, unit, "api-module", unit.uid, unit.uid, Model.summary(unit.doc))152}153154155fn memberPage(unit: &Model.Unit, member: &Model.Member, context: &Context) -> (Docgen.Page, Array[Docgen.Diagnostic]) {156 let page = Catalog.pageOf(&context.options, member.uid)157 var body = heading(start(), kindName(member.kind), member.name)158 let moduleHref = Paths.between(page, Catalog.pageOf(&context.options, unit.uid))159 body = emit(body, "<p class=\"api-namespace\">Module <a class=\"xref\" href=\"" + Paths.escape(moduleHref) + "\">" + Paths.escape(unit.uid) + "</a></p>\n")160 body = detail(body, unit, member, page, context)161 finish(body, page, unit, "api-member", member.name, member.uid, Model.summary(member.doc))162}163164165fn typePage(unit: &Model.Unit, member: &Model.Member, context: &Context) -> (Docgen.Page, Array[Docgen.Diagnostic]) {166 let page = Catalog.pageOf(&context.options, member.uid)167 var body = heading(start(), kindName(member.kind), member.name)168 let moduleHref = Paths.between(page, Catalog.pageOf(&context.options, unit.uid))169 body = emit(body, "<p class=\"api-namespace\">Module <a class=\"xref\" href=\"" + Paths.escape(moduleHref) + "\">" + Paths.escape(unit.uid) + "</a></p>\n")170 body = declaration(body, unit, member.signature, page, context)171 body = source(body, unit, member.line, context)172 body = documentation(body, member.doc, unit, page, member.line, context)173 if member.kind == "record" || member.kind == "union" {174 let (id, title, column) = if member.kind == "record" { ("fields", "Fields", "Type") } else { ("variants", "Variants", "Holds") }175 body = emit(section(body, 2, id, title), "<div class=\"table-wrapper\"><table class=\"api-table\"><thead><tr><th>Name</th><th>" + column + "</th><th>Description</th></tr></thead><tbody>\n")176 for inner in member.members {177 body = emit(body, "<tr id=\"" + Paths.escape(inner.name) + "\"><td><code>" + Paths.escape(inner.name) + "</code></td><td><code>" + Signature.html(unit, inner.returns, &context.references, page) + "</code></td><td>")178 body = emit(documentation(body, inner.doc, unit, page, inner.line, context), "</td></tr>\n")179 }180 body = emit(body, "</tbody></table></div>\n")181 }182 if member.kind == "trait" {183 if !member.members.isEmpty() {184 body = section(body, 2, "methods", "Methods")185 for method in member.members { body = detail(body, unit, &method, page, context) }186 }187 let implementors = implementorsOf(&member.uid, context)188 if !implementors.isEmpty() {189 body = emit(section(body, 2, "implementations", "Implementations"), "<ul class=\"api-list\">\n")190 for (owner, target) in implementors { body = emit(body, "<li><code>" + Signature.html(&owner, target, &context.references, page) + "</code></li>\n") }191 body = emit(body, "</ul>\n")192 }193 } else {194 let traits = traitsOf(member, context)195 if !traits.isEmpty() {196 body = emit(section(body, 2, "implements", "Implemented traits"), "<ul class=\"api-list\">\n")197 for contract in traits { body = emit(body, "<li><code>" + Signature.html(unit, contract, &context.references, page) + "</code></li>\n") }198 body = emit(body, "</ul>\n")199 }200 }201 let related = unit.members.filter(|other: Model.Member| other.kind == "function" && mentions(other.signature, member.name))202 if !related.isEmpty() { body = summaryTable(section(body, 2, "related", "Related functions"), &related, page, context) }203 finish(body, page, unit, "api-type", member.name, member.uid, Model.summary(member.doc))204}205206207fn implementorsOf(uid: &Str, context: &Context) -> Array[(Model.Unit, Str)] {208 var result: Array[(Model.Unit, Str)] = []209 for owner in context.units {210 for held in owner.implementations {211 if Signature.resolve(&owner, held.contract, &context.references) == *uid { result = result.push((owner, held.target)) }212 }213 }214 result215}216217218fn traitsOf(member: &Model.Member, context: &Context) -> Array[Str] {219 var result: Array[Str] = []220 for owner in context.units {221 for held in owner.implementations {222 let target = Signature.resolve(&owner, held.target.split("[")[0], &context.references)223 if target == member.uid && !held.contract.isEmpty() {224 let contract = Signature.resolve(&owner, held.contract, &context.references)225 let written = if contract.isEmpty() { held.contract } else { contract }226 if !result.contains(written) { result = result.push(written) }227 }228 }229 }230 result231}232233234fn mentions(signature: Str, name: Str) -> Bool {235 var cleaned = signature236 for separator in ["(", ")", "[", "]", ",", ":", "&", "->", "|", "\{", "\}", "="] { cleaned = cleaned.replace(separator, " ") }237 for word in cleaned.split(" ") {238 let parts = word.split(".")239 if parts[parts.length() - 1] == name { return true }240 }241 false242}243