
View.pudu
Pudu211 lines13.0 KB
1/** @Docgen.Site.View — everything a layout shows for one page, as template metadata */2module PuduLangDocgen.Site.View34import Std.Json as Json5import Std.Option as Option6import PuduLangDocgen as Docgen7import PuduLangDocgen.Markdown.Sanitize as Sanitize8import PuduLangDocgen.Meta as Meta9import PuduLangDocgen.Paths as Paths10import PuduLangDocgen.Site.Strings as Strings1112/** @Docgen.Site.Frame — navigation and history around a page */13export type Frame = {14 navbar: Array[Docgen.TocItem],15 toc: Array[Docgen.TocItem],16 trail: Array[Docgen.TocItem],17 previous: Option[Docgen.TocItem],18 next: Option[Docgen.TocItem],19 editUrl: Str,20 updated: Str,21 features: Array[Str],22 root: Str,23 pdf: Str,24 year: Str,25 tocPath: Str,26 navPath: Str27}282930const MERMAID: Str = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"313233const MATHJAX: Str = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"34353637383940export fn view(page: &Docgen.Page, frame: &Frame) -> Docgen.Meta {41 let meta = page.meta42 let root = if frame.root.isEmpty() { Paths.rootOf(page.path) } else { frame.root }43 let local = |href: Str| if href.isEmpty() || Paths.remote(href) { href } else { root + href }44 let siteName = Meta.textOr(&meta, "_appName", Meta.textOr(&meta, "_appTitle", "Documentation"))45 let lang = Meta.textOr(&meta, "_lang", "")46 let layout = Meta.textOr(&meta, "_layout", "default")47 let plain = layout == "landing" || layout == "chromeless"48 let showToc = !plain && !Meta.flag(&meta, "_disableToc") && !frame.toc.isEmpty()49 let showAffix = !plain && !Meta.flag(&meta, "_disableAffix") && page.headings.filter(|found: Docgen.Heading| found.level == 2 || found.level == 3).length() > 150 let showNavbar = !Meta.flag(&meta, "_disableNavbar") && !frame.navbar.isEmpty()51 let editUrl = if Meta.flag(&meta, "_disableContribution") { "" } else { frame.editUrl }52 let hidePager = plain || Meta.flag(&meta, "_disableNextArticle")53 let previous = if hidePager { None } else { frame.previous }54 let next = if hidePager { None } else { frame.next }55 let baseUrl = Meta.textOr(&meta, "_baseUrl", "")56 let canonical = if baseUrl.isEmpty() { "" } else { (if baseUrl.endsWith("/") { baseUrl } else { baseUrl + "/" }) + page.path }57 let footer = Meta.textOr(&meta, "_appFooter", "")58 let footerLinks = links(&meta, "_footerLinks", root)59 let holder = Meta.textOr(&meta, "_copyright", "")60 let copyright = if holder.isEmpty() { "" } else { "© " + (if frame.year.isEmpty() { "" } else { frame.year + " " }) + holder }61 let pdfUrl = if frame.pdf.isEmpty() { "" } else { Paths.between(page.path, frame.pdf) }62 let fields: Array[(Str, Docgen.Meta)] = [63 ("lang", Docgen.Text(lang)), ("root", Docgen.Text(root)), ("kind", Docgen.Text(page.kind)),64 ("title", Docgen.Text(page.title)), ("siteTitle", Docgen.Text(Meta.textOr(&meta, "_appTitle", ""))),65 ("siteName", Docgen.Text(siteName)), ("description", Docgen.Text(Meta.textOr(&meta, "description", page.summary))),66 ("noindex", Docgen.Flag(Meta.flag(&meta, "_noindex"))), ("canonical", Docgen.Text(canonical)),67 ("favicon", Docgen.Text(local(Meta.textOr(&meta, "_appFaviconPath", "")))), ("logo", Docgen.Text(local(Meta.textOr(&meta, "_appLogoPath", "")))),68 ("customStyle", Docgen.Text(local(Meta.textOr(&meta, "_appStyle", "")))), ("customScript", Docgen.Text(local(Meta.textOr(&meta, "_appScript", "")))),69 ("searchEnabled", Docgen.Flag(Meta.textOr(&meta, "_enableSearch", "true") != "false")),70 ("showToc", Docgen.Flag(showToc)), ("showAffix", Docgen.Flag(showAffix)), ("showNavbar", Docgen.Flag(showNavbar)),71 ("showBreadcrumb", Docgen.Flag(!plain && !Meta.flag(&meta, "_disableBreadcrumb"))), ("showTocFilter", Docgen.Flag(!Meta.flag(&meta, "_disableTocFilter"))),72 ("layout", Docgen.Text(layout)), ("chromeless", Docgen.Flag(layout == "chromeless")),73 ("showFooter", Docgen.Flag(layout != "chromeless" && !Meta.flag(&meta, "_disableFooter"))),74 ("logoUrl", Docgen.Text(Meta.textOr(&meta, "_appLogoUrl", root + "index.html"))),75 ("keywords", Docgen.Text(listed(&meta, "keywords"))), ("author", Docgen.Text(Meta.textOr(&meta, "author", ""))),76 ("ogType", Docgen.Text(if layout == "landing" || page.path == "index.html" { "website" } else { "article" })),77 ("image", Docgen.Text(absolute(Meta.textOr(&meta, "image", Meta.textOr(&meta, "_appOgImagePath", "")), baseUrl, root))),78 ("twitter", Docgen.Text(Meta.textOr(&meta, "_twitterSite", ""))), ("themeColor", Docgen.Text(Meta.textOr(&meta, "_themeColor", ""))),79 ("touchIcon", Docgen.Text(local(Meta.textOr(&meta, "_appTouchIconPath", "")))),80 ("extraMeta", Docgen.Items(Meta.fieldsOf(&meta, "_meta").map(fn(entry: (Str, Docgen.Meta)) -> Docgen.Meta {81 let (name, content) = entry82 Docgen.Fields([("name", Docgen.Text(name)), ("content", content)])83 }))),84 ("structuredData", Docgen.Text(structured(page, frame, siteName, baseUrl))), ("analytics", Docgen.Text(Meta.textOr(&meta, "_googleAnalyticsTagId", ""))),85 ("pdfUrl", Docgen.Text(pdfUrl)),86 ("_rel", Docgen.Text(root)), ("_path", Docgen.Text(page.path)), ("_tocPath", Docgen.Text(frame.tocPath)),87 ("_tocRel", Docgen.Text(if frame.tocPath.isEmpty() { "" } else { Paths.rootOf(page.path) + Paths.directoryOf(frame.tocPath) })),88 ("_navPath", Docgen.Text(frame.navPath)), ("_navRel", Docgen.Text(if frame.navPath.isEmpty() { "" } else { Paths.between(page.path, frame.navPath) })),89 ("navbar", Docgen.Items(navbar(page, frame))), ("toc", Docgen.Text(if showToc { tocHtml(&frame.toc, page.path, &frame.trail) } else { "" })),90 ("breadcrumb", Docgen.Items(breadcrumb(page, frame, siteName))),91 ("hasActions", Docgen.Flag(!plain && (!editUrl.isEmpty() || !frame.updated.isEmpty() || !pdfUrl.isEmpty()))), ("editUrl", Docgen.Text(editUrl)), ("lastModified", Docgen.Text(frame.updated)),92 ("hasPager", Docgen.Flag(Option.isSome(&previous) || Option.isSome(&next))), ("previous", neighbor(&previous, page.path)), ("next", neighbor(&next, page.path)),93 ("headings", Docgen.Items(page.headings.filter(|found: Docgen.Heading| found.level == 2 || found.level == 3).map(|found: Docgen.Heading| Docgen.Fields([("id", Docgen.Text(found.id)), ("title", Docgen.Text(found.title)), ("level", Docgen.Whole(found.level))])))),94 ("body", Docgen.Text(page.body)), ("footer", Docgen.Text(Sanitize.clean(footer))),95 ("hasFooterLinks", Docgen.Flag(!footerLinks.isEmpty())), ("footerLinks", Docgen.Items(footerLinks)), ("copyright", Docgen.Text(copyright)),96 ("math", Docgen.Flag(frame.features.contains("math"))), ("mathScript", Docgen.Text(Meta.textOr(&meta, "_mathScript", MATHJAX))),97 ("mermaid", Docgen.Flag(frame.features.contains("mermaid"))), ("mermaidScript", Docgen.Text(Meta.textOr(&meta, "_mermaidScript", MERMAID))),98 ("text", Strings.forLanguage(lang, &Meta.fieldsOf(&meta, "_text")))99 ]100 Docgen.Fields(fields)101}102103104105fn links(meta: &Array[(Str, Docgen.Meta)], key: Str, root: Str) -> Array[Docgen.Meta] {106 var result: Array[Docgen.Meta] = []107 match Meta.get(meta, key) {108 case Some(Docgen.Items(entries)) => {109 for entry in entries {110 match entry {111 case Docgen.Fields(fields) => {112 let name = Meta.textOr(&fields, "name", "")113 let href = Meta.textOr(&fields, "href", "")114 if !name.isEmpty() && !href.isEmpty() && Paths.href(href) {115 result = result.push(Docgen.Fields([("name", Docgen.Text(name)), ("href", Docgen.Text(if Paths.remote(href) { href } else { root + href }))]))116 }117 }118 case _ => {}119 }120 }121 }122 case _ => {}123 }124 result125}126127128fn listed(meta: &Array[(Str, Docgen.Meta)], key: Str) -> Str {129 match Meta.get(meta, key) {130 case Some(Docgen.Items(values)) => values.map(|value: Docgen.Meta| match value { case Docgen.Text(written) => written case _ => "" }).filter(|written: Str| !written.isEmpty()).join(", ")131 case Some(Docgen.Text(written)) => written132 case _ => ""133 }134}135136137fn absolute(path: Str, baseUrl: Str, root: Str) -> Str {138 if path.isEmpty() || Paths.remote(path) { return path }139 if baseUrl.isEmpty() { return root + path }140 (if baseUrl.endsWith("/") { baseUrl } else { baseUrl + "/" }) + path141}142143144fn structured(page: &Docgen.Page, frame: &Frame, siteName: Str, baseUrl: Str) -> Str {145 if baseUrl.isEmpty() { return "" }146 let base = if baseUrl.endsWith("/") { baseUrl } else { baseUrl + "/" }147 var items = [Json.object(&[("@type", Json.Text("ListItem")), ("position", Json.Number(1)), ("name", Json.Text(siteName)), ("item", Json.Text(base + "index.html"))])]148 var position = 2149 for item in frame.trail {150 if !item.href.isEmpty() && !Paths.remote(item.href) {151 items = items.push(Json.object(&[("@type", Json.Text("ListItem")), ("position", Json.Number(position)), ("name", Json.Text(item.title)), ("item", Json.Text(base + Paths.pathOf(item.href)))]))152 position = position + 1153 }154 }155 let data = Json.object(&[("@context", Json.Text("https://schema.org")), ("@type", Json.Text("BreadcrumbList")), ("itemListElement", Json.list(&items))])156 Json.encode(&data).replace("</", "<\\/")157}158159160export fn tocHtml(items: &Array[Docgen.TocItem], page: Str, trail: &Array[Docgen.TocItem]) -> Str {161 var pieces = ["<ul>"]162 for item in *items {163 let open = item.expanded || trail.contains(item)164 let classes = (if item.children.isEmpty() { "" } else { "has-children" }) + (if open && !item.children.isEmpty() { " expanded" } else { "" })165 pieces = pieces.push(if classes.isEmpty() { "<li>" } else { "<li class=\"" + classes + "\">" })166 if item.href.isEmpty() && item.children.isEmpty() {167 pieces = pieces.push("<span class=\"toc-heading\">" + Paths.escape(item.title) + "</span>")168 } else if item.href.isEmpty() {169 pieces = pieces.push("<button type=\"button\" class=\"toc-group\" aria-expanded=\"" + (if open { "true" } else { "false" }) + "\">" + Paths.escape(item.title) + "</button>")170 } else {171 let href = if Paths.remote(item.href) { item.href } else { Paths.between(page, item.href) }172 let current = if Paths.pathOf(item.href) == page { " class=\"active\" aria-current=\"page\"" } else { "" }173 pieces = pieces.push("<a href=\"" + Paths.escape(href) + "\"" + current + ">" + Paths.escape(item.title) + "</a>")174 }175 if !item.children.isEmpty() { pieces = pieces.push(tocHtml(&item.children, page, trail)) }176 pieces = pieces.push("</li>")177 }178 pieces.push("</ul>").join("")179}180181182fn navbar(page: &Docgen.Page, frame: &Frame) -> Array[Docgen.Meta] {183 frame.navbar.map(fn(item: Docgen.TocItem) -> Docgen.Meta {184 let href = if Paths.remote(item.href) || item.href.isEmpty() { item.href } else { Paths.between(page.path, item.href) }185 let active = (!frame.trail.isEmpty() && frame.trail[0].title == item.title) || Paths.pathOf(item.href) == page.path186 Docgen.Fields([("name", Docgen.Text(item.title)), ("href", Docgen.Text(href)), ("active", Docgen.Flag(active))])187 })188}189190191fn breadcrumb(page: &Docgen.Page, frame: &Frame, siteName: Str) -> Array[Docgen.Meta] {192 var entries = [Docgen.Fields([("name", Docgen.Text(siteName)), ("href", Docgen.Text(Paths.between(page.path, "index.html")))])]193 let trail = if frame.trail.isEmpty() { [Docgen.TocItem{title: page.title, href: page.path, uid: "", expanded: false, children: []}] } else { frame.trail }194 var index = 0195 for item in trail {196 let last = index == trail.length() - 1197 let href = if last || item.href.isEmpty() || Paths.remote(item.href) { "" } else { Paths.between(page.path, item.href) }198 entries = entries.push(Docgen.Fields([("name", Docgen.Text(if last { page.title } else { item.title })), ("href", Docgen.Text(href))]))199 index = index + 1200 }201 entries202}203204205fn neighbor(item: &Option[Docgen.TocItem], page: Str) -> Docgen.Meta {206 match item {207 case Some(found) => Docgen.Fields([("name", Docgen.Text(found.title)), ("href", Docgen.Text(Paths.between(page, found.href)))])208 case None => Docgen.Nothing209 }210}211