Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-docgen

Documentation publishing for Pudu: articles, API references, navigation, search, and static websites

0.1.0Apache-2.01

InstallClose

Meta.pudu

Pudu150 lines5.4 KB

GitHub ↗
1/** @Docgen.Meta.Module — metadata values from configuration, files, and front matter */2module PuduLangDocgen.Meta34import Std.Glob as Glob5import Std.Json as Json6import Std.Yaml as Yaml7import PuduLangDocgen as Docgen89/** @Docgen.Meta.Rule — one metadata key assigned to every path a glob matches */10export type Rule = { pattern: Str, key: Str, value: Docgen.Meta }1112/// A JSON value as metadata; fractional numbers keep their written text.13export fn fromJson(value: &Json.Json) -> Docgen.Meta {14  match value {15    case Json.Null => Docgen.Nothing16    case Json.Boolean(held) => Docgen.Flag(held)17    case Json.Number(count) => Docgen.Whole(count)18    case Json.Fractional(written) => Docgen.Text(written)19    case Json.Text(written) => Docgen.Text(written)20    case Json.List(members) => Docgen.Items(members.map(|member: Json.Json| fromJson(&member)))21    case Json.Object(entries) => Docgen.Fields(entries.map(fn(entry: (Str, Json.Json)) -> (Str, Docgen.Meta) {22          let (key, held) = entry23          (key, fromJson(&held))24        }))25  }26}2728/// A YAML value as metadata; fractional numbers keep their written text.29export fn fromYaml(value: &Yaml.Yaml) -> Docgen.Meta {30  match value {31    case Yaml.Empty => Docgen.Nothing32    case Yaml.Boolean(held) => Docgen.Flag(held)33    case Yaml.Whole(count) => Docgen.Whole(count)34    case Yaml.Fractional(written) => Docgen.Text(written)35    case Yaml.Text(written) => Docgen.Text(written)36    case Yaml.List(members) => Docgen.Items(members.map(|member: Yaml.Yaml| fromYaml(&member)))37    case Yaml.Mapping(entries) => Docgen.Fields(entries.map(fn(entry: (Str, Yaml.Yaml)) -> (Str, Docgen.Meta) {38          let (key, held) = entry39          (key, fromYaml(&held))40        }))41  }42}4344/// Metadata as JSON, preserving key order.45export fn toJson(value: &Docgen.Meta) -> Json.Json {46  match value {47    case Docgen.Nothing => Json.Null48    case Docgen.Flag(held) => Json.Boolean(held)49    case Docgen.Whole(count) => Json.Number(count)50    case Docgen.Text(written) => Json.Text(written)51    case Docgen.Items(members) => Json.List(members.map(|member: Docgen.Meta| toJson(&member)))52    case Docgen.Fields(entries) => Json.Object(entries.map(fn(entry: (Str, Docgen.Meta)) -> (Str, Json.Json) {53          let (key, held) = entry54          (key, toJson(&held))55        }))56  }57}5859/// The value stored under a key, if any.60export fn get(fields: &Array[(Str, Docgen.Meta)], key: Str) -> Option[Docgen.Meta] {61  for (name, held) in *fields {62    if name == key { return Some(held) }63  }64  None65}6667/// The text stored under a key; numbers and flags answer their written form.68export fn text(fields: &Array[(Str, Docgen.Meta)], key: Str) -> Option[Str] {69  match get(fields, key) {70    case Some(Docgen.Text(written)) => Some(written)71    case Some(Docgen.Whole(count)) => Some(show(count))72    case Some(Docgen.Flag(held)) => Some(if held { "true" } else { "false" })73    case _ => None74  }75}7677/// The text under a key, or the fallback when it is missing or not scalar.78export fn textOr(fields: &Array[(Str, Docgen.Meta)], key: Str, fallback: Str) -> Str {79  match text(fields, key) {80    case Some(written) => written81    case None => fallback82  }83}8485/// Whether a key holds `true`, or the text `true`.86export fn flag(fields: &Array[(Str, Docgen.Meta)], key: Str) -> Bool {87  match get(fields, key) {88    case Some(Docgen.Flag(held)) => held89    case Some(Docgen.Text(written)) => written == "true"90    case _ => false91  }92}9394/// The fields of a nested object under a key; other values answer no fields.95export fn fieldsOf(fields: &Array[(Str, Docgen.Meta)], key: Str) -> Array[(Str, Docgen.Meta)] {96  match get(fields, key) {97    case Some(Docgen.Fields(entries)) => entries98    case _ => []99  }100}101102/// The fields with a key set to a value, replacing an earlier value in place.103export fn put(fields: &Array[(Str, Docgen.Meta)], key: Str, value: Docgen.Meta) -> Array[(Str, Docgen.Meta)] {104  var result: Array[(Str, Docgen.Meta)] = []105  var replaced = false106  for (name, held) in *fields {107    if name == key {108      result = result.push((key, value))109      replaced = true110    } else { result = result.push((name, held)) }111  }112  if replaced { result } else { result.push((key, value)) }113}114115/// Later fields override earlier ones; nested objects merge key by key.116export fn merge(base: &Array[(Str, Docgen.Meta)], over: &Array[(Str, Docgen.Meta)]) -> Array[(Str, Docgen.Meta)] {117  var result = *base118  for (key, held) in *over {119    let combined = match (get(&result, key), held) {120      case (Some(Docgen.Fields(left)), Docgen.Fields(right)) => Docgen.Fields(merge(&left, &right))121      case (_, other) => other122    }123    result = put(&result, key, combined)124  }125  result126}127128/// Rules from a `fileMetadata` object: each key maps glob patterns to values.129export fn rulesOf(fields: &Array[(Str, Docgen.Meta)]) -> Result[Array[Rule], Str] {130  var rules: Array[Rule] = []131  for (key, held) in *fields {132    match held {133      case Docgen.Fields(patterns) => {134        for (pattern, value) in patterns { rules = rules.push(Rule{pattern: pattern, key: key, value: value}) }135      }136      case _ => { return Err("file metadata " + key + " must map glob patterns to values") }137    }138  }139  Ok(rules)140}141142/// The metadata every matching rule assigns to a path; later rules win.143export fn forPath(rules: &Array[Rule], path: Str) -> Array[(Str, Docgen.Meta)] {144  var result: Array[(Str, Docgen.Meta)] = []145  for rule in *rules {146    if Glob.matches(rule.pattern, path) { result = merge(&result, &[(rule.key, rule.value)]) }147  }148  result149}150