
Site.pudu
Pudu204 lines11.5 KB
1/** @Docgen.Build.Site — navigation, page frames, templated pages, and site-wide files */2module PuduLangDocgen.Build.Site34import Std.Json as Json5import Std.Map as Map6import PuduLangDocgen.Configuration as Configuration7import PuduLangDocgen as Docgen8import PuduLangDocgen.Meta as Meta9import PuduLangDocgen.Navigation as Navigation10import PuduLangDocgen.Navigation.Resolve as Resolve11import PuduLangDocgen.Paths as Paths12import PuduLangDocgen.References as References13import PuduLangDocgen.Site.Redirect as Redirect14import PuduLangDocgen.Site.Search as Search15import PuduLangDocgen.Site.Sitemap as Sitemap16import PuduLangDocgen.Site.View as View17import PuduLangDocgen.Theme as Theme1819/** @Docgen.Build.Book — a resolved table of contents with where it publishes and its metadata */20export type Book = { output: Str, items: Array[Docgen.TocItem], meta: Array[(Str, Docgen.Meta)], nested: Bool }2122/** @Docgen.Build.Context — what every page of the site is framed with */23export type Context = {24 config: Configuration.Config,25 books: Array[Book],26 look: Theme.Look,27 history: Map[Str, Str],28 repository: Configuration.Contribution,29 global: Array[(Str, Docgen.Meta)],30 generator: Str,31 today: Str32}333435const EDIT_PATTERNS: Array[(Str, Str)] = [36 ("github", "\{repo\}/blob/\{branch\}/\{path\}"), ("gitlab", "\{repo\}/-/blob/\{branch\}/\{path\}"),37 ("vso", "\{repo\}?path=/\{path\}&version=GB\{branch\}"), ("azure", "\{repo\}?path=/\{path\}&version=GB\{branch\}"),38 ("bitbucket", "\{repo\}/src/\{branch\}/\{path\}")39]4041424344export fn books(tocs: &Array[(Str, Str, Navigation.Toc)], links: &Resolve.Links) -> (Array[Book], Array[Docgen.Diagnostic]) {45 var referenced: Array[Str] = []46 for (_source, _output, toc) in *tocs {47 for item in flattened(&toc.items) {48 if Navigation.isToc(Paths.pathOf(item.href)) || item.href.endsWith("/") { referenced = referenced.push(item.href) }49 }50 }51 var result: Array[Book] = []52 var problems: Array[Docgen.Diagnostic] = []53 for (source, output, toc) in *tocs {54 if output.isEmpty() { continue }55 let (items, found) = Resolve.resolve(source, links)56 problems = problems.concat(found)57 let name = source.split("/")[source.split("/").length() - 1]58 let folder = Paths.directoryOf(source).split("/")59 let nested = referenced.filter(|href: Str| href.endsWith(name) || href.endsWith(folder[folder.length() - 1] + "/")).length() > 060 result = result.push(Book{output: Resolve.outputOf(output), items: items, meta: toc.meta, nested: nested})61 }62 (result, problems)63}646566export fn rendered(pages: &Array[Docgen.Page], context: &Context) -> Array[Docgen.Artifact] {67 let root = context.books.filter(|book: Book| book.output == "toc.json")68 let navbar = if root.isEmpty() { [] } else { root[0].items.map(|item: Docgen.TocItem| Docgen.TocItem{..item, children: []}) }69 let rootItems = if root.isEmpty() { [] } else { root[0].items }70 let candidates = context.books.map(|book: Book| Resolve.Candidate{output: book.output, order: orderOf(&book), pages: Resolve.pagesOf(&book.items)})71 var result: Array[Docgen.Artifact] = []72 for page in *pages {73 if page.kind == "redirect" {74 result = result.push(Docgen.Artifact{path: page.path, content: Redirect.page(page.title, Meta.textOr(&page.meta, "redirect_url", "index.html"))})75 continue76 }77 let governing = Resolve.governing(page.path, &candidates)78 let book = context.books.filter(|held: Book| held.output == governing)79 let items = if book.isEmpty() { [] } else { book[0].items }80 let fromRoot = Resolve.trail(&rootItems, page.path)81 let (previous, next) = Resolve.neighbors(&items, page.path)82 let features = ["math", "mermaid"].filter(|feature: Str| page.body.contains("class=\"" + feature))83 let pdf = if book.isEmpty() || !Meta.flag(&book[0].meta, "pdf") { "" } else { pdfOf(&book[0]) }84 let frame = View.Frame {85 navbar: navbar, toc: items, trail: if fromRoot.isEmpty() { Resolve.trail(&items, page.path) } else { fromRoot },86 previous: previous, next: next, editUrl: editUrl(&page, context), updated: Map.getOr(&context.history, page.source, ""),87 features: features, root: if page.kind == "not-found" { basePath(&page.meta) } else { "" }, pdf: pdf, year: context.today.take(4), tocPath: governing,88 navPath: if root.isEmpty() { "" } else { "toc.json" }89 }90 let view = View.view(&page, &frame)91 result = result.push(Docgen.Artifact{path: page.path, content: Theme.page(&context.look, &view)})92 if !context.config.exportViewModel.isEmpty() { result = result.push(Docgen.Artifact{path: modelPath(context.config.exportViewModel, context.config.output, page.path, ".view.json"), content: Json.encodePretty(&Meta.toJson(&view))}) }93 if !context.config.exportRawModel.isEmpty() { result = result.push(Docgen.Artifact{path: modelPath(context.config.exportRawModel, context.config.output, page.path, ".raw.json"), content: Json.encodePretty(&raw(&page))}) }94 }95 result96}979899100export fn files(pages: &Array[Docgen.Page], references: &Array[Docgen.Reference], context: &Context) -> Array[Docgen.Artifact] {101 var result: Array[Docgen.Artifact] = []102 for book in context.books { result = result.push(Docgen.Artifact{path: book.output, content: Json.encodePretty(&Navigation.toJson(&relative(&book.items, book.output)))}) }103 if Meta.textOr(&context.global, "_enableSearch", "true") != "false" { result = result.push(Docgen.Artifact{path: "index.json", content: Search.index(pages)}) }104 let base = Meta.textOr(&context.global, "_baseUrl", "")105 match context.config.sitemap {106 case Some(options) => {107 let modified = pages.map(|page: Docgen.Page| (page.path, Map.getOr(&context.history, page.source, context.today)))108 result = result.push(Docgen.Artifact{path: "sitemap.xml", content: Sitemap.render(pages, &options, &modified)})109 }110 case None => {}111 }112 let mapBase = match context.config.sitemap {113 case Some(options) => options.baseUrl114 case None => base115 }116 result.push(Docgen.Artifact{path: "xrefmap.yml", content: References.toYaml(references, mapBase)})117}118119120export fn notFound(global: &Array[(Str, Docgen.Meta)]) -> Docgen.Page {121 let body = "<h1 id=\"page-not-found\">Page not found</h1>\n<p>The page you requested does not exist or has moved.</p>\n<p><a href=\"index.html\">Go to the home page</a></p>\n"122 Docgen.Page{path: "404.html", source: "", kind: "not-found", title: "Page not found", uid: "", summary: "", body: body, headings: [], links: [], meta: Meta.merge(global, &[("_noindex", Docgen.Flag(true)), ("_disableToc", Docgen.Flag(true)), ("_disableBreadcrumb", Docgen.Flag(true))])}123}124125126export fn manifest(pages: &Array[Docgen.Page], resources: &Array[(Str, Str)], generator: Str, outputs: &Array[Str]) -> Str {127 let entries = pages.map(|page: Docgen.Page| Json.object(&[("type", Json.Text(page.kind)), ("source", Json.Text(page.source)), ("output", Json.Text(page.path))])).concat(resources.map(fn(entry: (Str, Str)) -> Json.Json {128 let (source, output) = entry129 Json.object(&[("type", Json.Text("resource")), ("source", Json.Text(source)), ("output", Json.Text(output))])130 }))131 Json.encodePretty(&Json.object(&[("generator", Json.Text("pudu-lang-docgen")), ("version", Json.Text(generator)), ("files", Json.list(&entries)), ("outputs", Json.list(&outputs.map(|path: Str| Json.Text(path))))]))132}133134135fn orderOf(book: &Book) -> Int {136 match Meta.get(&book.meta, "order") {137 case Some(Docgen.Whole(value)) => value138 case _ => if book.nested { 100 } else { 0 }139 }140}141142143export fn pdfOf(book: &Book) -> Str {144 let name = Meta.textOr(&book.meta, "pdfFileName", "toc.pdf")145 let directory = Paths.directoryOf(book.output)146 if directory.isEmpty() { name } else { directory + "/" + name }147}148149150fn flattened(items: &Array[Docgen.TocItem]) -> Array[Docgen.TocItem] {151 var result: Array[Docgen.TocItem] = []152 for item in *items { result = result.push(item).concat(flattened(&item.children)) }153 result154}155156157fn relative(items: &Array[Docgen.TocItem], from: Str) -> Array[Docgen.TocItem] {158 items.map(|item: Docgen.TocItem| Docgen.TocItem{..item, href: if item.href.isEmpty() || Paths.remote(item.href) { item.href } else { Paths.between(from, item.href) }, children: relative(&item.children, from)})159}160161162fn editUrl(page: &Docgen.Page, context: &Context) -> Str {163 if page.source.isEmpty() { return "" }164 let contribute = Meta.fieldsOf(&page.meta, "_gitContribute")165 let repo = Meta.textOr(&contribute, "repo", context.repository.repo)166 if repo.isEmpty() { return "" }167 let branch = Meta.textOr(&contribute, "branch", context.repository.branch)168 let prefix = Meta.textOr(&contribute, "path", context.repository.path)169 let path = if prefix.isEmpty() { page.source } else { prefix + "/" + page.source }170 let written = Meta.textOr(&page.meta, "_gitUrlPattern", if repo.contains("gitlab") { "gitlab" } else if repo.contains("dev.azure.com") || repo.contains("visualstudio.com") { "vso" } else if repo.contains("bitbucket") { "bitbucket" } else { "github" })171 var pattern = written172 for (name, template) in EDIT_PATTERNS {173 if name == written { pattern = template }174 }175 let trimmed = if repo.endsWith("/") { repo.take(repo.length() - 1) } else if repo.endsWith(".git") { repo.take(repo.length() - 4) } else { repo }176 pattern.replace("\{repo\}", trimmed).replace("\{branch\}", branch).replace("\{path\}", path)177}178179180fn basePath(meta: &Array[(Str, Docgen.Meta)]) -> Str {181 let base = Meta.textOr(meta, "_baseUrl", "")182 if base.isEmpty() { return "/" }183 let afterScheme = if base.contains("://") { base.split("://")[1] } else { base }184 let slash = afterScheme.indexOf("/")185 let path = if slash < 0 { "/" } else { afterScheme.drop(slash) }186 if path.endsWith("/") { path } else { path + "/" }187}188189190fn modelPath(folder: Str, output: Str, page: Str, suffix: Str) -> Str {191 let inner = if folder == output { "" } else if folder.startsWith(output + "/") { folder.drop(output.length() + 1) + "/" } else { "_models/" }192 inner + page + suffix193}194195196fn raw(page: &Docgen.Page) -> Json.Json {197 Json.object(&[198 ("path", Json.Text(page.path)), ("source", Json.Text(page.source)), ("kind", Json.Text(page.kind)), ("title", Json.Text(page.title)),199 ("uid", Json.Text(page.uid)), ("summary", Json.Text(page.summary)), ("body", Json.Text(page.body)),200 ("headings", Json.list(&page.headings.map(|heading: Docgen.Heading| Json.object(&[("id", Json.Text(heading.id)), ("title", Json.Text(heading.title)), ("level", Json.Number(heading.level))])))),201 ("metadata", Meta.toJson(&Docgen.Fields(page.meta)))202 ])203}204