
Chapter.pudu
Pudu103 lines3.7 KB
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 }131415const SECTION_MARK: Str = "## "1617const TITLE_MARK: Str = "# "181920const FENCE_MARK: Str = "```"212223export 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}505152export 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}596061export 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}656667export fn find(chapters: &Array[Chapter], group: Str, slug: Str) -> Option[Chapter] {68 List.find(chapters, |chapter: Chapter| chapter.group == group && chapter.slug == slug)69}707172export fn inGroup(chapters: &Array[Chapter], group: Str) -> Array[Chapter] {73 chapters.filter(|chapter: Chapter| chapter.group == group)74}757677export fn sectionIn(chapter: &Chapter, wanted: Str) -> Option[Section] {78 let key = wanted.trim().toLower()79 List.find(§ionsOf(chapter), |candidate: Section| candidate.anchor == key || candidate.heading.toLower() == key)80}818283export 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}939495fn 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}9899100fn isWordCharacter(character: Char) -> Bool {101 Char.isAlphanumeric(character)102}103