
Sources.pudu
Pudu179 lines9.2 KB
1/** @Docgen.Build.Sources — project files mapped to outputs and read into typed documents */2module PuduLangDocgen.Build.Sources34import Std.Glob as Glob5import Std.Json as Json6import Std.List as List7import Std.Map as Map8import PuduLangDocgen.Configuration as Configuration9import PuduLangDocgen.Constants.Codes as Codes10import PuduLangDocgen as Docgen11import PuduLangDocgen.Markdown as Markdown12import PuduLangDocgen.Meta as Meta13import PuduLangDocgen.Navigation as Navigation14import PuduLangDocgen.Paths as Paths15import PuduLangDocgen.Rest.OpenApi as OpenApi16import PuduLangDocgen.Site.Gallery as Gallery17import PuduLangDocgen.Site.Landing as Landing18import PuduLangDocgen.Yaml as Yaml1920/** @Docgen.Build.Document — one content file read into the form its kind needs */21export type Document = Article(Markdown.Article) | Service(OpenApi.Service) | ApiPage(Docgen.Meta) | Catalog(Docgen.Meta) | Hub(Docgen.Meta) | Moved(Str)2223/** @Docgen.Build.Entry — a content file, where it publishes, and its merged metadata */24export type Entry = { source: Str, output: Str, meta: Array[(Str, Docgen.Meta)], document: Document }2526/** @Docgen.Build.Read — every content file read, with tables of contents apart */27export type Read = { entries: Array[Entry], tocs: Array[(Str, Str, Navigation.Toc)], diagnostics: Array[Docgen.Diagnostic] }282930const API_PAGE: Str = "#YamlMime:ApiPage"313233343536export fn mapped(mappings: &Array[Configuration.Mapping], paths: &Array[Str], skip: &Array[Str]) -> Array[(Str, Str)] {37 selected(mappings, paths, skip).map(fn(entry: (Str, Str, Array[(Str, Docgen.Meta)])) -> (Str, Str) {38 let (source, output, _meta) = entry39 (source, output)40 })41}424344export fn groupMeta(mappings: &Array[Configuration.Mapping], paths: &Array[Str], skip: &Array[Str]) -> Map[Str, Array[(Str, Docgen.Meta)]] {45 mapOf(selected(mappings, paths, skip).map(fn(entry: (Str, Str, Array[(Str, Docgen.Meta)])) -> (Str, Array[(Str, Docgen.Meta)]) {46 let (source, _output, meta) = entry47 (source, meta)48 }))49}505152fn selected(mappings: &Array[Configuration.Mapping], paths: &Array[Str], skip: &Array[Str]) -> Array[(Str, Str, Array[(Str, Docgen.Meta)])] {53 var result: Array[(Str, Str, Array[(Str, Docgen.Meta)])] = []54 var taken: Array[Str] = []55 for mapping in *mappings {56 for path in List.sorted(paths) {57 if taken.contains(path) || skip.filter(|folder: Str| path.startsWith(folder + "/")).length() > 0 { continue }58 let inner = if mapping.src.isEmpty() { path } else if path.startsWith(mapping.src + "/") { path.drop(mapping.src.length() + 1) } else { "" }59 if inner.isEmpty() || !Glob.matchesAny(&mapping.files, inner) || Glob.matchesAny(&mapping.exclude, inner) { continue }60 result = result.push((path, if mapping.dest.isEmpty() { inner } else { mapping.dest + "/" + inner }, mapping.meta))61 taken = taken.push(path)62 }63 }64 result65}66676869export fn metadata(config: &Configuration.Config, files: &Map[Str, Str]) -> (Array[(Str, Docgen.Meta)], Array[Meta.Rule], Array[Docgen.Diagnostic]) {70 var global = config.globalMetadata71 var rules = config.fileMetadata72 var problems: Array[Docgen.Diagnostic] = []73 for path in config.globalMetadataFiles {74 match object(path, files) {75 case Ok(fields) => { global = Meta.merge(&global, &fields) }76 case Err(problem) => { problems = problems.push(problem) }77 }78 }79 for path in config.fileMetadataFiles {80 match object(path, files) {81 case Ok(fields) => match Meta.rulesOf(&fields) {82 case Ok(found) => { rules = rules.concat(found) }83 case Err(reason) => { problems = problems.push(Docgen.error(Codes.CONFIG_INVALID, path, 1, reason)) }84 }85 case Err(problem) => { problems = problems.push(problem) }86 }87 }88 (Meta.merge(&global, &config.globalMetadata), rules, problems)89}9091929394export fn read(content: &Array[(Str, Str)], files: &Map[Str, Str], global: &Array[(Str, Docgen.Meta)], rules: &Array[Meta.Rule], alerts: &Array[Str], groups: &Map[Str, Array[(Str, Docgen.Meta)]]) -> Read {95 var entries: Array[Entry] = []96 var tocs: Array[(Str, Str, Navigation.Toc)] = []97 var problems: Array[Docgen.Diagnostic] = []98 for (source, output) in *content {99 let text = match Map.get(files, source) {100 case Some(found) => found101 case None => {102 problems = problems.push(Docgen.error(Codes.CONTENT_UNREADABLE, source, 1, "content file could not be read"))103 continue104 }105 }106 let inherited = Meta.merge(&Meta.merge(global, &Map.getOr(groups, source, [])), &Meta.forPath(rules, source))107 let lowered = source.toLower()108 if Navigation.isToc(source) {109 match Navigation.parse(source, text) {110 case Ok(found) => { tocs = tocs.push((source, output, Navigation.Toc{..found, meta: Meta.merge(&Meta.merge(global, &found.meta), &Meta.forPath(rules, source))})) }111 case Err(found) => { problems = problems.concat(found) }112 }113 } else if lowered.endsWith(".md") || lowered.endsWith(".markdown") {114 let article = Markdown.parse(source, text, files, alerts)115 problems = problems.concat(article.diagnostics)116 let meta = Meta.merge(&inherited, &article.meta)117 let target = Meta.textOr(&meta, "redirect_url", "")118 let document = if target.isEmpty() { Article(article) } else { Moved(target) }119 entries = entries.push(Entry{source: source, output: Paths.output(output), meta: meta, document: document})120 } else if lowered.endsWith(".yml") || lowered.endsWith(".yaml") || lowered.endsWith(".json") {121 match structured(source, text) {122 case Ok(value) => {123 if text.split("\n")[0].contains(Gallery.MARKER) {124 entries = entries.push(Entry{source: source, output: Paths.output(output), meta: inherited, document: Catalog(value)})125 } else if text.split("\n")[0].contains(Landing.MARKER) {126 entries = entries.push(Entry{source: source, output: Paths.output(output), meta: Meta.merge(&Meta.merge(&[("_layout", Docgen.Text("landing"))], &inherited), &declared(&value)), document: Hub(value)})127 } else if text.trim().startsWith(API_PAGE) {128 entries = entries.push(Entry{source: source, output: Paths.output(output), meta: inherited, document: ApiPage(value)})129 } else if OpenApi.recognizes(&value) {130 match OpenApi.read(&value) {131 case Ok(service) => { entries = entries.push(Entry{source: source, output: Paths.output(output), meta: inherited, document: Service(service)}) }132 case Err(reason) => { problems = problems.push(Docgen.error(Codes.CONTENT_INVALID, source, 1, reason)) }133 }134 } else { problems = problems.push(Docgen.warning(Codes.CONTENT_UNSUPPORTED, source, 1, "structured file is not an interface description, an API page, a catalog, or a landing page and is skipped")) }135 }136 case Err(reason) => { problems = problems.push(Docgen.error(Codes.CONTENT_INVALID, source, 1, reason)) }137 }138 } else { problems = problems.push(Docgen.warning(Codes.CONTENT_UNSUPPORTED, source, 1, "content file type is not supported; list it as a resource to copy it")) }139 }140 Read{entries: entries, tocs: tocs, diagnostics: problems}141}142143144fn structured(source: Str, text: Str) -> Result[Docgen.Meta, Str] {145 if source.toLower().endsWith(".json") {146 match Json.decode(text) {147 case Ok(found) => Ok(Meta.fromJson(&found))148 case Err(problem) => Err("not valid JSON: " + Json.explain(&problem))149 }150 } else {151 let decoded = Yaml.decode(text)152 match decoded {153 case Ok(found) => Ok(found)154 case Err(reason) => Err("not valid YAML: " + reason)155 }156 }157}158159160fn object(path: Str, files: &Map[Str, Str]) -> Result[Array[(Str, Docgen.Meta)], Docgen.Diagnostic] {161 let text = match Map.get(files, path) {162 case Some(found) => found163 case None => { return Err(Docgen.error(Codes.CONFIG_UNREADABLE, path, 1, "metadata file could not be read")) }164 }165 match structured(path, text) {166 case Ok(Docgen.Fields(fields)) => Ok(fields)167 case Ok(_) => Err(Docgen.error(Codes.CONFIG_INVALID, path, 1, "metadata file must hold an object"))168 case Err(reason) => Err(Docgen.error(Codes.CONFIG_UNREADABLE, path, 1, reason))169 }170}171172173fn declared(value: &Docgen.Meta) -> Array[(Str, Docgen.Meta)] {174 match value {175 case Docgen.Fields(fields) => Meta.fieldsOf(&fields, "metadata")176 case _ => []177 }178}179