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

Pages.pudu

Pudu243 lines12.9 KB

GitHub ↗
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] }1819/// Section titles of a module page, in page order, with the member kinds each lists.20const SECTIONS: Array[(Str, Str, Array[Str])] = [21  ("types", "Types", ["record", "union", "alias", "opaque"]), ("traits", "Traits", ["trait"]),22  ("functions", "Functions", ["function"]), ("constants", "Constants", ["constant"])23]2425/// Display names of member kinds.26const 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]3031/// The module page and one page per type the module declares.32export 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}4546/// The display name of a member kind.47export fn kindName(kind: Str) -> Str {48  for (key, name) in KIND_NAMES {49    if key == kind { return name }50  }51  kind52}5354/// An empty body.55fn start() -> Body { Body{pieces: [], headings: [], links: [], diagnostics: []} }5657/// A body with HTML appended.58fn emit(body: Body, text: Str) -> Body { Body{..body, pieces: body.pieces.push(text)} }5960/// A body with a titled section heading appended and recorded for the page outline.61fn 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}6667/// Documentation rendered as HTML with headings demoted below the page's own sections.68fn 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}8081/// The source link paragraph of a declaration, when a pattern is configured.82fn 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}8687/// A declaration in a code block with types linked.88fn 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}9192/// The page title block with the kind shown above the name.93fn 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}9697/// A page from a finished body.98fn 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}102103/// A table linking members to their pages or sections with their summaries.104fn 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}115116/// A function, method, or constant documented in its own section.117fn 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}136137/// The page of a module.138fn 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}153154/// The page of one function or constant.155fn 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}163164/// The page of a record, union, alias, opaque type, or trait.165fn 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}205206/// Types implementing a trait across all modules, with the module each implementation is in.207fn 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}216217/// Traits a type implements in any module.218fn 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}232233/// Whether a signature names a type as a whole word.234fn 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