
Catalog.pudu
Pudu186 lines9.1 KB
1/** @Docgen.Api.Catalog — selected declarations, their pages, identities, and navigation */2module PuduLangDocgen.Api.Catalog34import Std.Glob as Glob5import Std.List as List6import Std.Regex as Regex7import PuduLangDocgen.Api.Model as Model8import PuduLangDocgen as Docgen9import PuduLangDocgen.Meta as Meta1011/** @Docgen.Api.Rule — an include or exclude decision for identities a pattern matches */12export type Rule = { include: Bool, pattern: Regex.Regex, kind: Str }1314/** @Docgen.Api.Options — where reference pages go and which declarations they show */15export type Options = {16 dest: Str,17 includePrivate: Bool,18 include: Array[Str],19 exclude: Array[Str],20 rules: Array[Rule],21 sourceUrl: Str,22 sourceExclude: Array[Str],23 nested: Bool,24 separate: Bool,25 skipMarkup: Bool,26 alphabetic: Bool,27 categories: Str28}293031export fn defaults() -> Options {32 Options{dest: "api", includePrivate: false, include: [], exclude: [], rules: [], sourceUrl: "", sourceExclude: [], nested: false, separate: false, skipMarkup: false, alphabetic: false, categories: "none"}33}343536const CATEGORIES: Array[(Str, Array[Str])] = [37 ("Types", ["record", "union", "alias", "opaque"]), ("Traits", ["trait"]), ("Functions", ["function"]), ("Constants", ["constant"])38]39404142export fn rules(value: &Docgen.Meta) -> Result[Array[Rule], Str] {43 let fields = match value {44 case Docgen.Fields(found) => found45 case _ => { return Err("filter must be an object with apiRules") }46 }47 let listed = match Meta.get(&fields, "apiRules") {48 case Some(Docgen.Items(found)) => found49 case _ => { return Err("filter needs an apiRules list") }50 }51 var result: Array[Rule] = []52 for held in listed {53 let entry = match held {54 case Docgen.Fields(found) => found55 case _ => { return Err("each api rule must be an object") }56 }57 let include = Meta.get(&entry, "include") != None58 let body = Meta.fieldsOf(&entry, if include { "include" } else { "exclude" })59 let written = Meta.textOr(&body, "uidRegex", "")60 if written.isEmpty() { return Err("each api rule needs a uidRegex under include or exclude") }61 let pattern = match Regex.compile(written) {62 case Ok(found) => found63 case Err(problem) => { return Err("uidRegex " + written + ": " + Regex.explain(&problem)) }64 }65 result = result.push(Rule{include: include, pattern: pattern, kind: Meta.textOr(&body, "type", "").toLower()})66 }67 Ok(result)68}69707172export fn select(units: &Array[Model.Unit], options: &Options) -> Array[Model.Unit] {73 var result: Array[Model.Unit] = []74 for unit in List.sortOn(units, |held: Model.Unit| held.uid) {75 if !wanted(options, unit.uid) { continue }76 if !ruled(options, unit.uid, "module") { continue }77 let members = unit.members.filter(|member: Model.Member| (member.exported || options.includePrivate) && wanted(options, member.uid) && ruled(options, member.uid, member.kind))78 let kept = Model.Unit{..unit, members: members.map(fn(member: Model.Member) -> Model.Member {79 let inner = member.members.filter(|held: Model.Member| wanted(options, held.uid) && ruled(options, held.uid, held.kind))80 let ordered = if options.alphabetic && member.kind == "union" { List.sortOn(&inner, |held: Model.Member| held.name) } else { inner }81 Model.Member{..member, members: ordered}82 }) }83 if !kept.members.isEmpty() || !kept.doc.isEmpty() { result = result.push(kept) }84 }85 result86}878889export fn pageOf(options: &Options, uid: Str) -> Str {90 if options.dest.isEmpty() { uid + ".html" } else { options.dest + "/" + uid + ".html" }91}9293949596export fn references(units: &Array[Model.Unit], options: &Options) -> Array[Docgen.Reference] {97 var result: Array[Docgen.Reference] = []98 for unit in *units {99 let modulePage = pageOf(options, unit.uid)100 result = result.push(Docgen.Reference{uid: unit.uid, name: lastName(unit.uid), fullName: unit.uid, href: modulePage, kind: "module"})101 for member in unit.members {102 let typed = Model.isType(member.kind)103 let href = if typed || options.separate { pageOf(options, member.uid) } else { modulePage + "#" + member.name }104 result = result.push(Docgen.Reference{uid: member.uid, name: member.name, fullName: member.uid, href: href, kind: member.kind})105 if typed {106 for inner in member.members {107 result = result.push(Docgen.Reference{uid: inner.uid, name: inner.name, fullName: inner.uid, href: pageOf(options, member.uid) + "#" + inner.name, kind: inner.kind})108 }109 }110 }111 }112 result113}114115116117118export fn toc(units: &Array[Model.Unit], options: &Options) -> Array[Docgen.TocItem] {119 let flat = units.map(fn(unit: Model.Unit) -> Docgen.TocItem {120 let shown = unit.members.filter(|member: Model.Member| Model.isType(member.kind) || options.separate)121 let ordered = List.sortOn(&shown, |held: Model.Member| held.name)122 let entry = |member: Model.Member| Docgen.TocItem{title: member.name, href: pageOf(options, member.uid), uid: member.uid, expanded: false, children: []}123 var children: Array[Docgen.TocItem] = []124 if options.categories == "none" { children = ordered.map(entry) } else {125 for (heading, kinds) in CATEGORIES {126 let group = ordered.filter(|member: Model.Member| kinds.contains(member.kind)).map(entry)127 if !group.isEmpty() {128 children = if options.categories == "nested" { children.push(Docgen.TocItem{title: heading, href: "", uid: "", expanded: false, children: group}) } else { children.push(Docgen.TocItem{title: heading, href: "", uid: "", expanded: false, children: []}).concat(group) }129 }130 }131 }132 Docgen.TocItem{title: unit.uid, href: pageOf(options, unit.uid), uid: unit.uid, expanded: false, children: children}133 })134 if options.nested { nest(&flat, "") } else { flat }135}136137138139fn nest(flat: &Array[Docgen.TocItem], prefix: Str) -> Array[Docgen.TocItem] {140 var heads: Array[Str] = []141 for item in *flat {142 let rest = if prefix.isEmpty() { item.uid } else if item.uid.startsWith(prefix + ".") { item.uid.drop(prefix.length() + 1) } else { "" }143 if !rest.isEmpty() && !heads.contains(rest.split(".")[0]) { heads = heads.push(rest.split(".")[0]) }144 }145 heads.map(fn(head: Str) -> Docgen.TocItem {146 let full = if prefix.isEmpty() { head } else { prefix + "." + head }147 let below = nest(flat, full)148 var own = Docgen.TocItem{title: head, href: "", uid: "", expanded: false, children: []}149 for item in *flat {150 if item.uid == full { own = Docgen.TocItem{..item, title: head} }151 }152 Docgen.TocItem{..own, children: below.concat(own.children)}153 })154}155156157158export fn sourceLink(options: &Options, path: Str, line: Int) -> Str {159 if options.sourceUrl.isEmpty() || Glob.matchesAny(&options.sourceExclude, path) { "" } else { options.sourceUrl.replace("\{path\}", path).replace("\{line\}", show(line)) }160}161162163export fn lastName(uid: Str) -> Str {164 let parts = uid.split(".")165 parts[parts.length() - 1]166}167168169170fn ruled(options: &Options, uid: Str, kind: Str) -> Bool {171 let family = if kind == "module" { "module" } else if Model.isType(kind) { "type" } else { kind }172 for rule in options.rules {173 let fits = rule.kind.isEmpty() || rule.kind == family || (rule.kind == "member" && family != "module" && family != "type")174 if fits && Regex.isMatch(&rule.pattern, uid) { return rule.include }175 }176 true177}178179180181fn wanted(options: &Options, uid: Str) -> Bool {182 let inside = !options.include.filter(|pattern: Str| pattern.startsWith(uid + ".")).isEmpty()183 if !options.include.isEmpty() && !Glob.matchesAny(&options.include, uid) && !inside { return false }184 !Glob.matchesAny(&options.exclude, uid)185}186