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

Sheet.pudu

Pudu162 lines7.9 KB

GitHub ↗
1/** @Docgen.Api.Sheet — structured API pages written by any language's converter */2module PuduLangDocgen.Api.Sheet34import Std.Option as Option5import PuduLangDocgen.Constants.Codes as Codes6import PuduLangDocgen as Docgen7import PuduLangDocgen.Markdown.Highlight as Highlight8import PuduLangDocgen.Markdown as Markdown9import PuduLangDocgen.Markdown.Phrase as Phrase10import PuduLangDocgen.Meta as Meta11import PuduLangDocgen.Paths as Paths1213/** @Docgen.Api.SheetPage — the rendered title, body, outline, and findings of a page */14export type Rendered = { title: Str, body: Str, headings: Array[Docgen.Heading], links: Array[Docgen.Link], diagnostics: Array[Docgen.Diagnostic] }1516/// Heading block keys and their levels.17const HEADINGS: Array[(Str, Int)] = [("h1", 1), ("h2", 2), ("h3", 3), ("h4", 4), ("h5", 5), ("h6", 6)]1819/// API heading block keys and the heading levels they render as.20const APIS: Array[(Str, Int)] = [("api1", 1), ("api2", 2), ("api3", 3), ("api4", 4)]2122/// A page from an API page document: `title`, optional `languageId`, and a `body` of blocks23/// named `h1`–`h6`, `api1`–`api4`, `markdown`, `code`, `facts`, `parameters`, `list`, and24/// `inheritance`. Unknown blocks are reported and skipped.25export fn render(value: &Docgen.Meta, context: &Phrase.Scope) -> Result[Rendered, Str] {26  let fields = match value {27    case Docgen.Fields(found) => found28    case _ => { return Err("API page must be an object") }29  }30  let title = Meta.textOr(&fields, "title", "")31  if title.isEmpty() { return Err("API page needs a title") }32  let language = Meta.textOr(&fields, "languageId", "")33  let blocks = match Meta.get(&fields, "body") {34    case Some(Docgen.Items(found)) => found35    case _ => { return Err("API page needs a body list") }36  }37  var result = Rendered{title: title, body: "", headings: [], links: [], diagnostics: []}38  var pieces: Array[Str] = []39  for block in blocks {40    let entry = match block {41      case Docgen.Fields(found) => found42      case _ => { return Err("each API page block must be an object") }43    }44    let (html, next) = piece(&entry, language, context, result)45    pieces = pieces.push(html)46    result = next47  }48  Ok(Rendered{..result, body: pieces.join("")})49}5051/// One block as HTML with the page facts it adds.52fn piece(entry: &Array[(Str, Docgen.Meta)], language: Str, context: &Phrase.Scope, result: Rendered) -> (Str, Rendered) {53  for (key, level) in HEADINGS {54    match Meta.text(entry, key) {55      case Some(text) => { return heading(level, text, Meta.textOr(entry, "id", Paths.slug(text)), "", result) }56      case None => {}57    }58  }59  for (key, level) in APIS {60    match Meta.text(entry, key) {61      case Some(text) => {62        var badges: Array[Str] = []63        for (flag, label) in [("deprecated", "Deprecated"), ("preview", "Preview")] {64          match Meta.get(entry, flag) {65            case Some(Docgen.Flag(true)) => { badges = badges.push("<span class=\"badge " + flag + "\">" + label + "</span>") }66            case Some(Docgen.Text(reason)) => { badges = badges.push("<span class=\"badge " + flag + "\" title=\"" + Paths.escape(reason) + "\">" + label + "</span>") }67            case _ => {}68          }69        }70        let src = Meta.textOr(entry, "src", "")71        let source = if src.isEmpty() || !Paths.href(src) { "" } else { "<a class=\"api-source-link external\" href=\"" + Paths.escape(src) + "\">View source</a>" }72        return heading(level, text, Meta.textOr(entry, "id", Paths.slug(text)), badges.join("") + source, result)73      }74      case None => {}75    }76  }77  match Meta.text(entry, "markdown") {78    case Some(text) => {79      let rendered = Markdown.fragment(text, context, 0)80      return (rendered.html, Rendered{..result, links: result.links.concat(rendered.links), diagnostics: result.diagnostics.concat(rendered.diagnostics)})81    }82    case None => {}83  }84  match Meta.text(entry, "code") {85    case Some(text) => {86      let chosen = Meta.textOr(entry, "languageId", language)87      return ("<div class=\"api-signature\"><pre><code class=\"lang-" + Paths.escape(chosen) + "\">" + Highlight.lines(chosen, text).join("\n") + "</code></pre></div>\n", result)88    }89    case None => {}90  }91  match Meta.get(entry, "facts") {92    case Some(Docgen.Items(facts)) => {93      var rows: Array[Str] = []94      for fact in facts {95        let held = match fact { case Docgen.Fields(found) => found case _ => [] }96        rows = rows.push("<div class=\"fact\"><dt>" + Paths.escape(Meta.textOr(&held, "name", "")) + "</dt><dd>" + inline(&Option.unwrapOr(Meta.get(&held, "value"), Docgen.Nothing), context) + "</dd></div>")97      }98      return ("<dl class=\"api-facts\">" + rows.join("") + "</dl>\n", result)99    }100    case _ => {}101  }102  match Meta.get(entry, "parameters") {103    case Some(Docgen.Items(parameters)) => { return parameterList(&parameters, context, result) }104    case _ => {}105  }106  for (key, joiner) in [("list", ", "), ("inheritance", " → ")] {107    match Meta.get(entry, key) {108      case Some(Docgen.Items(items)) => { return ("<p class=\"api-" + key + "\">" + items.map(|item: Docgen.Meta| inline(&item, context)).join(joiner) + "</p>\n", result) }109      case _ => {}110    }111  }112  let keys = entry.map(fn(pair: (Str, Docgen.Meta)) -> Str {113      let (name, _held) = pair114      name115    })116  ("", Rendered{..result, diagnostics: result.diagnostics.push(Docgen.warning(Codes.CONTENT_UNSUPPORTED, context.source, 1, "unknown API page block: " + keys.join(", ")))})117}118119/// A heading with an anchor and optional trailing markup, recorded in the page outline.120fn heading(level: Int, text: Str, id: Str, trailing: Str, result: Rendered) -> (Str, Rendered) {121  let tag = "h" + show(level)122  let html = "<" + tag + " id=\"" + Paths.escape(id) + "\">" + Paths.escape(text) + trailing + "</" + tag + ">\n"123  (html, Rendered{..result, headings: result.headings.push(Docgen.Heading{id: id, title: text, level: level})})124}125126/// A parameter list with types, defaults, flags, and Markdown descriptions.127fn parameterList(parameters: &Array[Docgen.Meta], context: &Phrase.Scope, result: Rendered) -> (Str, Rendered) {128  var held = result129  var rows: Array[Str] = []130  for parameter in *parameters {131    let fields = match parameter { case Docgen.Fields(found) => found case _ => [] }132    let kind = match Meta.get(&fields, "type") {133      case Some(value) => " <span class=\"parameter-type\">" + inline(&value, context) + "</span>"134      case None => ""135    }136    let defaultValue = Meta.textOr(&fields, "default", "")137    let fallback = if defaultValue.isEmpty() { "" } else { " = <code>" + Paths.escape(defaultValue) + "</code>" }138    let flags = (if Option.isSome(&Meta.get(&fields, "deprecated")) { " <span class=\"badge deprecated\">Deprecated</span>" } else { "" }) + (if Option.isSome(&Meta.get(&fields, "preview")) { " <span class=\"badge preview\">Preview</span>" } else { "" })139    let description = Markdown.fragment(Meta.textOr(&fields, "description", ""), context, 0)140    held = Rendered{..held, links: held.links.concat(description.links), diagnostics: held.diagnostics.concat(description.diagnostics)}141    rows = rows.push("<dt><code>" + Paths.escape(Meta.textOr(&fields, "name", "")) + "</code>" + kind + fallback + flags + "</dt><dd>" + description.html + "</dd>")142  }143  ("<dl class=\"api-parameters\">" + rows.join("") + "</dl>\n", held)144}145146/// An inline value: text, a `{text, url}` link, or a list of both.147fn inline(value: &Docgen.Meta, context: &Phrase.Scope) -> Str {148  match value {149    case Docgen.Text(written) => Paths.escape(written)150    case Docgen.Items(spans) => spans.map(|span: Docgen.Meta| inline(&span, context)).join("")151    case Docgen.Fields(fields) => {152      let text = Paths.escape(Meta.textOr(&fields, "text", ""))153      let url = Meta.textOr(&fields, "url", "")154      if url.isEmpty() || !Paths.href(url) { text } else {155        let href = if Paths.remote(url) || url.startsWith("#") { url } else { Paths.between(context.page, url) }156        "<a href=\"" + Paths.escape(href) + "\">" + text + "</a>"157      }158    }159    case _ => ""160  }161}162