
SyncDocs.pudu
Pudu157 lines6.1 KB
1/** @Tools.SyncDocs.Generator — compiles published prose into Pudu source */2module SyncDocs34import Std.Env as Env5import Std.Io as Io6import Std.List as List7import Std.Option as Option8import Std.Text as Text91011const DOCS_OUTPUT: Str = "src/PuduLangMcp/Generated/Docs.pudu"1213const HEADER: Str = "/** @Generated.Docs.Corpus — published Pudu prose, generated; do not edit */\n"141516const PAGES: Array[(Str, Str)] = [17 ("conduct", "CODE_OF_CONDUCT.md"),18 ("contributing", "CONTRIBUTING.md"),19 ("security", "SECURITY.md"),20 ("community", "website/pages/community.md"),21 ("privacy", "website/pages/privacy.md"),22 ("brand", "website/pages/brand.md")23]242526type Document = { group: Str, slug: Str, title: Str, markdown: Str }272829fn main() -> Int {30 let checkout = match Env.at(0) {31 case Some(given) => given32 case None => { return failed("usage: pudu run tools/SyncDocs.pudu <pudu-lang-checkout> [revision]") }33 }34 let revision = Option.unwrapOr(Env.at(1), "unknown")35 var documents: Array[Document] = []36 match chapters(checkout + "/website/docs") {37 case Ok(found) => { documents = documents.concat(found) }38 case Err(problem) => { return failed(problem) }39 }40 for page in PAGES {41 match Io.read(checkout + "/" + page[1]) {42 case Ok(text) => { documents = documents.push(Document{group: "pages", slug: page[0], title: titleOf(text, page[0]), markdown: text}) }43 case Err(problem) => { return failed("cannot read " + page[1] + ": " + problem) }44 }45 }46 match releases(checkout) {47 case Ok(found) => { documents = documents.concat(found) }48 case Err(problem) => { return failed(problem) }49 }50 match examples(checkout + "/website/playground/examples") {51 case Ok(found) => { documents = documents.concat(found) }52 case Err(problem) => { return failed(problem) }53 }54 let docsModule = HEADER + "module PuduLangMcp.Generated.Docs\n\nimport PuduLangMcp.Domain.Docs.Chapter as Chapter\n\nexport const REVISION: Str = " + literal(revision) + "\n\nexport const CHAPTERS: Array[Chapter.Chapter] = [\n" + documents.map(|held: Document| documentLiteral(&held)).join(",\n") + "\n]\n"55 match Io.write(DOCS_OUTPUT, docsModule) {56 case Ok(_) => ()57 case Err(problem) => { return failed("cannot write " + DOCS_OUTPUT + ": " + problem) }58 }59 let _said = Io.writeLine("wrote " + show(documents.length()) + " documents to " + DOCS_OUTPUT)60 061}626364fn chapters(directory: Str) -> Result[Array[Document], Str] {65 let names = sortedWith(directory, ".md") ?66 var found: Array[Document] = []67 for name in names {68 let text = readIn(directory, name) ?69 let slug = slugOf(name, ".md")70 found = found.push(Document{group: "docs", slug: slug, title: titleOf(text, slug), markdown: text})71 }72 if found.isEmpty() { Err("no documentation chapters in " + directory) } else { Ok(found) }73}747576fn releases(checkout: Str) -> Result[Array[Document], Str] {77 let series = sortedWith(checkout + "/packages/pudu", "") ?78 var found: Array[Document] = []79 for version in series {80 let directory = checkout + "/packages/pudu/" + version + "/release-notes"81 if Io.exists(directory) {82 for name in sortedWith(directory, ".md") ? {83 let text = readIn(directory, name) ?84 let number = name.replace(".md", "")85 found = found.push(Document{group: "releases", slug: number, title: "Pudu " + number, markdown: text})86 }87 }88 }89 Ok(found)90}919293fn examples(directory: Str) -> Result[Array[Document], Str] {94 let names = sortedWith(directory, ".pudu") ?95 var found: Array[Document] = []96 for name in names {97 let code = readIn(directory, name) ?98 let slug = slugOf(name, ".pudu")99 let first = Option.unwrapOr(List.first(&code.split("\n")), "")100 let title = if first.startsWith("// ") { first.drop(3).trim() } else { slug }101 found = found.push(Document{group: "examples", slug: slug, title: title, markdown: "# " + title + "\n\n```pudu\n" + code.trim() + "\n```\n"})102 }103 Ok(found)104}105106107fn sortedWith(directory: Str, suffix: Str) -> Result[Array[Str], Str] {108 match Io.list(directory) {109 case Ok(found) => Ok(List.sortBy(&found.filter(|name: Str| name.endsWith(suffix) && !name.startsWith(".")), fn(left: Str, right: Str) -> Bool { left < right }))110 case Err(problem) => Err("cannot list " + directory + ": " + problem)111 }112}113114115fn readIn(directory: Str, name: Str) -> Result[Str, Str] {116 match Io.read(directory + "/" + name) {117 case Ok(text) => Ok(text.replace("\r", ""))118 case Err(problem) => Err("cannot read " + directory + "/" + name + ": " + problem)119 }120}121122123fn slugOf(name: Str, extension: Str) -> Str {124 let stem = Text.stripSuffix(name, extension)125 let dash = stem.indexOf("-")126 if dash > 0 && isDigits(stem.take(dash)) { stem.drop(dash + 1) } else { stem }127}128129130fn titleOf(text: Str, fallback: Str) -> Str {131 for line in text.split("\n") {132 if line.startsWith("# ") { return line.drop(2).trim() }133 }134 fallback135}136137138fn isDigits(text: Str) -> Bool {139 !text.isEmpty() && text.chars().filter(|c: Char| c < '0' || c > '9').isEmpty()140}141142143fn documentLiteral(held: &Document) -> Str {144 " Chapter.Chapter\{group: " + literal(held.group) + ", slug: " + literal(held.slug) + ", title: " + literal(held.title) + ", markdown: " + literal(held.markdown) + "\}"145}146147148fn literal(text: Str) -> Str {149 "\"" + text.replace("\\", "\\\\").replace("\"", "\\\"").replace("\{", "\\\{").replace("\}", "\\\}").replace("\r", "").replace("\n", "\\n").replace("\t", "\\t") + "\""150}151152153fn failed(message: Str) -> Int {154 let _said = Io.writeErrorLine("SyncDocs: " + message)155 1156}157