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

Rules.pudu

Pudu83 lines4.8 KB

GitHub ↗
1/** @Docgen.Configuration.Rules — cross-field rules a read configuration must satisfy */2module PuduLangDocgen.Configuration.Rules34import PuduLangDocgen.Constants.Codes as Codes5import PuduLangDocgen as Docgen6import PuduLangDocgen.Paths as Paths7import PuduLangDocgen.References as References8import PuduLangValidator.Result as Validation9import PuduLangValidator.Rule as Rule10import PuduLangValidator.Rules.Collection as Each11import PuduLangValidator.Validator as Validator1213/** @Docgen.Configuration.Settings — the configuration values these rules judge */14export type Settings = {15  output: Str,16  sitemap: Bool,17  baseUrl: Str,18  priority: Str,19  changefreq: Str,20  xref: Array[Str],21  services: Array[Str],22  destinations: Array[Str],23  repository: Str,24  renderer: Array[Str]25}2627/// Change frequencies crawlers understand.28export const FREQUENCIES: Array[Str] = ["always", "hourly", "daily", "weekly", "monthly", "yearly", "never"]2930/// Failures of the rules as configuration diagnostics located by property path.31export fn check(file: Str, settings: &Settings) -> Array[Docgen.Diagnostic] {32  let outcome = Validator.validate(&validator(), *settings)33  outcome.errors.map(|failure: Validation.Failure| Docgen.error(Codes.CONFIG_INVALID, file, 1, failure.propertyName + ": " + failure.errorMessage))34}3536/// The rules: an output folder other than the project folder, an absolute sitemap address37/// with a priority from 0 to 1 and a known change frequency, reference maps that are web38/// addresses or project paths, reference services that are web addresses holding `\{uid\}`, distinct API destinations, a web repository for edit links,39/// and a PDF renderer that names its `\{output\}`.40fn validator() -> Validator.Validator[Settings] {41  let output = Rule.build(Rule.withMessage(Rule.must(Rule.ruleFor("build.output", |held: Settings| held.output), |value: Str| !value.isEmpty()), "must name a folder other than the project folder"))42  let baseUrl = Rule.build(Rule.when(Rule.withMessage(Rule.must(Rule.ruleFor("build.sitemap.baseUrl", |held: Settings| held.baseUrl), |value: Str| web(value)), "must be an absolute http or https address"), |held: Settings| held.sitemap))43  let priority = Rule.build(Rule.when(Rule.withMessage(Rule.must(Rule.ruleFor("build.sitemap.priority", |held: Settings| held.priority), |value: Str| value.isEmpty() || fraction(value)), "must be a number from 0.0 to 1.0"), |held: Settings| held.sitemap))44  let frequency = Rule.build(Rule.when(Rule.withMessage(Rule.must(Rule.ruleFor("build.sitemap.changefreq", |held: Settings| held.changefreq), |value: Str| value.isEmpty() || FREQUENCIES.contains(value)), "must be one of " + FREQUENCIES.join(", ")), |held: Settings| held.sitemap))45  let xref = Each.buildEach(Each.withMessage(Each.must(Each.ruleForEach("build.xref", |held: Settings| held.xref), |value: Str| web(value) || Paths.relative(value)), "must be a web address or a relative path inside the project"))46  let services = Each.buildEach(Each.withMessage(Each.must(Each.ruleForEach("build.xrefService", |held: Settings| held.services), |value: Str| web(value) && value.contains(References.UID_SLOT)), "must be a web address holding \{uid\}"))47  let destinations = Rule.build(Rule.withMessage(Rule.must(Rule.ruleFor("metadata.dest", |held: Settings| held.destinations), |values: Array[Str]| distinct(&values)), "each API source needs its own destination folder"))48  let repository = Rule.build(Rule.withMessage(Rule.must(Rule.ruleFor("build.gitContribute.repo", |held: Settings| held.repository), |value: Str| value.isEmpty() || web(value)), "must be an http or https address"))49  let renderer = Rule.build(Rule.withMessage(Rule.must(Rule.ruleFor("pdf.renderer", |held: Settings| held.renderer), |values: Array[Str]| values.isEmpty() || (!values[0].isEmpty() && values.filter(|part: Str| part.contains("\{output\}")).length() > 0)), "must name a program and an argument holding \{output\}"))50  var result = Validator.create()51  for rule in [output, baseUrl, priority, frequency, xref, services, destinations, repository, renderer] { result = Validator.add(result, rule) }52  result53}5455/// Whether text is an absolute HTTP or HTTPS address.56fn web(value: Str) -> Bool {57  let lowered = value.toLower()58  (lowered.startsWith("https://") || lowered.startsWith("http://")) && Paths.href(value)59}6061/// Whether text is a decimal number from 0 to 1.62fn fraction(text: Str) -> Bool {63  let parts = text.split(".")64  if parts.length() > 2 || parts[0].isEmpty() { return false }65  for part in parts {66    for character in part.chars() {67      if character < '0' || character > '9' { return false }68    }69  }70  if parts[0] == "1" { return parts.length() == 1 || parts[1].replace("0", "").isEmpty() }71  parts[0] == "0"72}7374/// Whether no text appears twice.75fn distinct(values: &Array[Str]) -> Bool {76  var seen: Array[Str] = []77  for value in *values {78    if seen.contains(value) { return false }79    seen = seen.push(value)80  }81  true82}83