
Sheet.pudu
Pudu162 lines7.9 KB
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] }151617const HEADINGS: Array[(Str, Int)] = [("h1", 1), ("h2", 2), ("h3", 3), ("h4", 4), ("h5", 5), ("h6", 6)]181920const APIS: Array[(Str, Int)] = [("api1", 1), ("api2", 2), ("api3", 3), ("api4", 4)]2122232425export 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}505152fn 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(¶meters, 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}118119120fn 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}125126127fn 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}145146147fn 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