Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-mcp

Model Context Protocol server for Pudu language documentation and compiler tools

0.1.1Apache-2.01

InstallClose

Chapter.pudu

Pudu103 lines3.7 KB

GitHub ↗
1/** @Domain.Docs.Chapter.Aggregate — published documents and their sections */2module PuduLangMcp.Domain.Docs.Chapter34import Std.Char as Char5import Std.List as List6import Std.Text as Text78/** @Domain.Docs.Chapter.Chapter — one published document by group */9export type Chapter = { group: Str, slug: Str, title: Str, markdown: Str }1011/** @Domain.Docs.Chapter.Section — a document cut at second-level headings */12export type Section = { group: Str, chapter: Str, chapterTitle: Str, heading: Str, anchor: Str, text: Str }1314/// The heading prefix that starts a section.15const SECTION_MARK: Str = "## "16/// The heading prefix of a document's own title, which is not section text.17const TITLE_MARK: Str = "# "1819/// The line prefix that opens and closes a code fence.20const FENCE_MARK: Str = "```"2122/// A document cut into sections at its second-level headings.23export fn sectionsOf(chapter: &Chapter) -> Array[Section] {24  var sections: Array[Section] = []25  var heading = chapter.title26  var body: Array[Str] = []27  var fenced = false28  var opened = false29  for line in chapter.markdown.split("\n") {30    if line.startsWith(FENCE_MARK) { fenced = !fenced }31    if !fenced && line.startsWith(TITLE_MARK) {32      continue33    }34    if !fenced && line.startsWith(SECTION_MARK) {35      if opened || !Text.isBlank(Text.join(&body, "\n")) {36        sections = sections.push(section(chapter, heading, &body))37      }38      heading = line.drop(SECTION_MARK.length()).trim()39      body = []40      opened = true41    } else {42      body = body.push(line)43    }44  }45  if opened || !Text.isBlank(Text.join(&body, "\n")) {46    sections = sections.push(section(chapter, heading, &body))47  }48  sections49}5051/// Every section of every document, in corpus order.52export fn allSections(chapters: &Array[Chapter]) -> Array[Section] {53  var sections: Array[Section] = []54  for chapter in *chapters {55    sections = sections.concat(sectionsOf(&chapter))56  }57  sections58}5960/// The address of a heading: lowercase words joined by dashes.61export fn anchorOf(heading: Str) -> Str {62  let spaced = heading.toLower().chars().map(|character: Char| if isWordCharacter(character) { Text.singleton(character) } else { " " })63  Text.join(&Text.words(spaced.join("")), "-")64}6566/// The document with a group and slug.67export fn find(chapters: &Array[Chapter], group: Str, slug: Str) -> Option[Chapter] {68  List.find(chapters, |chapter: Chapter| chapter.group == group && chapter.slug == slug)69}7071/// The documents of one group, in corpus order.72export fn inGroup(chapters: &Array[Chapter], group: Str) -> Array[Chapter] {73  chapters.filter(|chapter: Chapter| chapter.group == group)74}7576/// The section of a document matching a heading or anchor.77export fn sectionIn(chapter: &Chapter, wanted: Str) -> Option[Section] {78  let key = wanted.trim().toLower()79  List.find(&sectionsOf(chapter), |candidate: Section| candidate.anchor == key || candidate.heading.toLower() == key)80}8182/// One line per document with its sections indented beneath it.83export fn tableOfContents(chapters: &Array[Chapter]) -> Str {84  var lines: Array[Str] = []85  for chapter in *chapters {86    lines = lines.push(chapter.group + "/" + chapter.slug + " — " + chapter.title)87    for held in sectionsOf(&chapter) {88      if held.heading != chapter.title { lines = lines.push("  " + held.heading + " (#" + held.anchor + ")") }89    }90  }91  Text.join(&lines, "\n")92}9394/// A section built from its heading and body lines.95fn section(chapter: &Chapter, heading: Str, body: &Array[Str]) -> Section {96  Section{group: chapter.group, chapter: chapter.slug, chapterTitle: chapter.title, heading: heading, anchor: anchorOf(heading), text: Text.join(body, "\n").trim()}97}9899/// Whether a character is kept in an anchor.100fn isWordCharacter(character: Char) -> Bool {101  Char.isAlphanumeric(character)102}103