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

Landing.pudu

Pudu130 lines7.0 KB

GitHub ↗
1/** @Docgen.Site.Landing — hub pages with a banner, highlighted links, and topic lists */2module PuduLangDocgen.Site.Landing34import Std.Map as Map5import PuduLangDocgen as Docgen6import PuduLangDocgen.Markdown.Phrase as Phrase7import PuduLangDocgen.Markdown.Syntax as Syntax8import PuduLangDocgen.Meta as Meta9import PuduLangDocgen.Paths as Paths1011/** @Docgen.Site.LandingPage — the rendered title, body, outline, and findings of a hub page */12export type Rendered = { title: Str, body: Str, headings: Array[Docgen.Heading], links: Array[Docgen.Link], diagnostics: Array[Docgen.Diagnostic] }1314/// Marker on the first line of a landing file.15export const MARKER: Str = "YamlMime:Landing"1617/// Labels shown above highlighted links, by `itemType`.18const LABELS: Map[Str, Str] = mapOf([19    ("overview", "Overview"), ("get-started", "Get started"), ("quickstart", "Quickstart"), ("concept", "Concept"),20    ("tutorial", "Tutorial"), ("how-to-guide", "How-to guide"), ("reference", "Reference"), ("whats-new", "What's new"),21    ("download", "Download"), ("deploy", "Deploy"), ("architecture", "Architecture"), ("sample", "Sample")22  ])2324/// A hub page: `title`, an optional `summary` shown in the banner, `highlightedContent.items`25/// with `title`, `itemType`, and `url`, `conceptualContent.items` with a `title`, `links` of26/// `text` and `url`, and an optional `footerLink`, and `additionalContent.sections` whose27/// `items` have a `title`, `summary`, and `url`. Local addresses are checked like article links.28/// A `metadata` object sets the page's metadata, such as `uid` and `description`, when read.29export fn render(value: &Docgen.Meta, context: &Phrase.Scope) -> Result[Rendered, Str] {30  let fields = match value {31    case Docgen.Fields(found) => found32    case _ => { return Err("landing page must be an object") }33  }34  let title = Meta.textOr(&fields, "title", "")35  if title.isEmpty() { return Err("landing page needs a title") }36  var out = Phrase.start()37  var headings = [Docgen.Heading{id: Paths.slug(title), title: title, level: 1}]38  let summary = Meta.textOr(&fields, "summary", "")39  let lead = if summary.isEmpty() { "" } else { "<p class=\"landing-summary\">" + Paths.escape(summary) + "</p>" }40  var pieces = ["<section class=\"landing-hero\"><div class=\"landing-inner\"><h1 id=\"" + Paths.escape(Paths.slug(title)) + "\">" + Paths.escape(title) + "</h1>" + lead + "</div></section>"]41  let highlighted = objects(&Meta.fieldsOf(&fields, "highlightedContent"), "items", "highlighted item") ?42  if !highlighted.isEmpty() {43    var cards: Array[Str] = []44    for item in highlighted {45      let kind = Meta.textOr(&item, "itemType", "")46      let label = match Map.get(&LABELS, kind) {47        case Some(found) => found48        case None => { return Err("highlighted item has an unknown itemType: " + kind) }49      }50      let (linked, href) = link(out, &item, "url", context)51      out = linked52      cards = cards.push("<li class=\"landing-highlight\"><a href=\"" + Paths.escape(href) + "\"><span class=\"landing-kind\">" + Paths.escape(label) + "</span><span class=\"landing-link\">" + Paths.escape(required(&item, "title", "highlighted item") ?) + "</span></a></li>")53    }54    pieces = pieces.push("<ul class=\"landing-highlights landing-inner\">" + cards.join("") + "</ul>")55  }56  let conceptual = Meta.fieldsOf(&fields, "conceptualContent")57  let topics = objects(&conceptual, "items", "topic") ?58  if !topics.isEmpty() {59    let heading = Meta.textOr(&conceptual, "title", "")60    var cards: Array[Str] = []61    for topic in topics {62      let name = required(&topic, "title", "topic") ?63      var entries: Array[Str] = []64      for entry in objects(&topic, "links", "topic link") ? {65        let (linked, href) = link(out, &entry, "url", context)66        out = linked67        entries = entries.push("<li><a href=\"" + Paths.escape(href) + "\">" + Paths.escape(required(&entry, "text", "topic link") ?) + "</a></li>")68      }69      var card = ["<article class=\"landing-card\"><h3>" + Paths.escape(name) + "</h3><ul>" + entries.join("") + "</ul>"]70      let footer = Meta.fieldsOf(&topic, "footerLink")71      if !footer.isEmpty() {72        let (linked, href) = link(out, &footer, "url", context)73        out = linked74        card = card.push("<p class=\"landing-more\"><a href=\"" + Paths.escape(href) + "\">" + Paths.escape(required(&footer, "text", "footer link") ?) + "</a></p>")75      }76      cards = cards.push(card.push("</article>").join(""))77    }78    pieces = pieces.push(section(heading, "landing-topics", cards))79    if !heading.isEmpty() { headings = headings.push(Docgen.Heading{id: Paths.slug(heading), title: heading, level: 2}) }80  }81  for block in objects(&Meta.fieldsOf(&fields, "additionalContent"), "sections", "section") ? {82    let heading = Meta.textOr(&block, "title", "")83    var cards: Array[Str] = []84    for item in objects(&block, "items", "related item") ? {85      let (linked, href) = link(out, &item, "url", context)86      out = linked87      let text = Meta.textOr(&item, "summary", "")88      cards = cards.push("<article class=\"landing-card\"><h3><a href=\"" + Paths.escape(href) + "\">" + Paths.escape(required(&item, "title", "related item") ?) + "</a></h3>" + (if text.isEmpty() { "" } else { "<p>" + Paths.escape(text) + "</p>" }) + "</article>")89    }90    pieces = pieces.push(section(heading, "landing-related", cards))91    if !heading.isEmpty() { headings = headings.push(Docgen.Heading{id: Paths.slug(heading), title: heading, level: 2}) }92  }93  Ok(Rendered{title: title, body: pieces.join("\n"), headings: headings, links: out.links, diagnostics: out.diagnostics})94}9596/// A band of cards under an optional heading.97fn section(heading: Str, kind: Str, cards: Array[Str]) -> Str {98  let title = if heading.isEmpty() { "" } else { "<h2 id=\"" + Paths.escape(Paths.slug(heading)) + "\">" + Paths.escape(heading) + "</h2>" }99  "<section class=\"" + kind + " landing-inner\">" + title + "<div class=\"landing-cards\">" + cards.join("") + "</div></section>"100}101102/// The checked browser destination of an entry's address.103fn link(out: Phrase.Out, entry: &Array[(Str, Docgen.Meta)], key: Str, context: &Phrase.Scope) -> (Phrase.Out, Str) {104  Phrase.destination(out, &Syntax.Target{destination: Meta.textOr(entry, key, ""), title: "", line: 1}, context, context.source, false)105}106107/// Text under a key that must not be empty.108fn required(entry: &Array[(Str, Docgen.Meta)], key: Str, noun: Str) -> Result[Str, Str] {109  let found = Meta.textOr(entry, key, "")110  if found.isEmpty() { Err("each " + noun + " needs a " + key) } else { Ok(found) }111}112113/// The objects listed under a key; a missing key lists none.114fn objects(fields: &Array[(Str, Docgen.Meta)], key: Str, noun: Str) -> Result[Array[Array[(Str, Docgen.Meta)]], Str] {115  match Meta.get(fields, key) {116    case Some(Docgen.Items(listed)) => {117      var result: Array[Array[(Str, Docgen.Meta)]] = []118      for held in listed {119        match held {120          case Docgen.Fields(found) => { result = result.push(found) }121          case _ => { return Err("each " + noun + " must be an object") }122        }123      }124      Ok(result)125    }126    case None => Ok([])127    case _ => Err(key + " must be a list")128  }129}130