markdown.rs

8.3 kB · rust · 252 lines

1use super::records::record_by_id;2use super::{3    keys, sequence, Axis, Key, Measure, Record, Sequence, Tier, BUDGET, RECORDS, SPACES, TERMS,4};5use mrlymath::name::{Named, Sequence as SequenceName};6use std::collections::{BTreeMap, BTreeSet, HashSet};7use std::fs;8use std::path::Path;910const PROSE: &str = include_str!("prose.md");1112fn label(key: &Key) -> &'static str {13    match (key.dimension, key.code) {14        (2, 1) | (3, 1) => "dust",15        (2, 3) => "tree",16        (2, 6) | (3, 22) => "star",17        (2, 7) => "carpet",18        (2, 8) | (3, 128) => "point",19        (2, 9) | (3, 129) => "void",20        (2, 10) | (2, 12) | (3, 136) | (3, 160) | (3, 192) => "line",21        (2, 14) | (3, 232) => "net",22        (2, 11) => "corner and centre",23        (2, 15) | (3, 255) => "solid",24        (3, 23) => "sponge",25        _ => "design",26    }27}2829fn link(id: &str) -> String {30    format!("[{id}](https://oeis.org/{id})")31}3233fn spelled(row: &Sequence) -> String {34    let terms: Vec<String> = row.terms.iter().map(|term| term.to_string()).collect();35    let mut text = format!("`{}`", terms.join(", "));36    if row.capped {37        text.push_str(" to the cell budget");38    }39    text40}4142fn table(axis: Axis) -> String {43    let index = match axis {44        Axis::Level => "level 1",45        Axis::Side => "`k = 2`",46    };47    let mut out = format!("| id | design | key | closed form | terms from {index} | record | shift | status |\n|---|---|---|---|---|---|---|---|\n");48    let mut keyed: Vec<(Key, &Record)> = RECORDS49        .iter()50        .filter_map(|record| {51            record52                .key53                .filter(|key| key.axis == axis)54                .map(|key| (key, record))55        })56        .collect();57    keyed.sort_by_key(|(key, _)| (key.dimension, key.code, key.measure));58    for (key, record) in keyed {59        let row = sequence(&key, TERMS, BUDGET).expect("every keyed record reads");60        let (matched, shift) = row.record.expect("every keyed record matches");61        assert_eq!(matched.id, record.id, "the row reads its own record");62        let closed = row63            .closed64            .as_ref()65            .map_or_else(|| "none".to_string(), |form| format!("`{}`", form.text()));66        out.push_str(&format!(67            "| {} | {} | `{}` | {} | {} | {} | {} | **{}** |\n",68            key.id(),69            label(&key),70            key.name(),71            closed,72            spelled(&row),73            link(record.id),74            shift,75            record.status.text()76        ));77    }78    out.trim_end().to_string()79}8081fn tokens(text: &str) -> Vec<String> {82    let mut out = Vec::new();83    let mut rest = text;84    while let Some(at) = rest.find("sequence_") {85        let tail = &rest[at..];86        let end = tail87            .find(|c: char| !(c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '='))88            .unwrap_or(tail.len());89        out.push(tail[..end].to_string());90        rest = &tail[end..];91    }92    out93}9495fn citations() -> BTreeMap<String, BTreeSet<String>> {96    let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../research");97    let mut names: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();98    for folder in ["claims", "notes"] {99        let Ok(entries) = fs::read_dir(root.join(folder)) else {100            continue;101        };102        let mut paths: Vec<_> = entries.flatten().map(|entry| entry.path()).collect();103        paths.sort();104        for path in paths {105            let Ok(text) = fs::read_to_string(&path) else {106                continue;107            };108            let stem = path109                .file_stem()110                .and_then(|s| s.to_str())111                .unwrap_or_default();112            for token in tokens(&text) {113                names114                    .entry(token)115                    .or_default()116                    .insert(format!("{folder}/{stem}"));117            }118        }119    }120    names121}122123fn cited() -> String {124    let keyed: HashSet<String> = RECORDS125        .iter()126        .filter_map(|record| record.key.map(|key| key.name()))127        .collect();128    let mut out =129        String::from("| id | key | closed form | terms | cited by |\n|---|---|---|---|---|\n");130    for (name, by) in citations() {131        if keyed.contains(&name) {132            continue;133        }134        let named = SequenceName::from_file(&name).expect("every cited sequence name reads");135        let key = Key::new(136            named.code,137            named.dim,138            named.base,139            Measure::parse(&named.measure).expect("a cited measure is a measure"),140            Axis::parse(&named.axis).expect("a cited axis is an axis"),141        );142        let row = sequence(&key, TERMS, BUDGET).expect("every cited sequence reads");143        let closed = row144            .closed145            .as_ref()146            .map_or_else(|| "none".to_string(), |form| format!("`{}`", form.text()));147        let files: Vec<String> = by.into_iter().map(|file| format!("`{file}`")).collect();148        out.push_str(&format!(149            "| `{}` | `{}` | {} | {} | {} |\n",150            key.id(),151            key.name(),152            closed,153            spelled(&row),154            files.join(", ")155        ));156    }157    out.trim_end().to_string()158}159160fn records() -> String {161    let mut out = String::from(162        "| id | record | name | offset | first terms | key | shift | status |\n|---|---|---|---|---|---|---|---|\n",163    );164    for record in RECORDS {165        let (id, key, shift) = match record.key {166            Some(key) => (167                key.id(),168                format!("`{}`", key.name()),169                record.shift.to_string(),170            ),171            None => (String::new(), String::new(), String::new()),172        };173        out.push_str(&format!(174            "| {} | {} | {} | {} | `{}` | {} | {} | **{}** |\n",175            id,176            link(record.id),177            record.name,178            record.offset,179            record.terms,180            key,181            shift,182            record.status.text()183        ));184    }185    out.trim_end().to_string()186}187188fn tally() -> String {189    let designs: usize = SPACES190        .iter()191        .map(|&(dimension, base)| {192            super::designs(dimension, base)193                .expect("the ledger spaces are walkable")194                .len()195        })196        .sum();197    format!(198        "The registry walks {designs} designs across {} dimension and base pairs and holds {} closed rows and {} convolved rows of {TERMS} terms each; the grid tiers render on demand within a budget of {BUDGET} cells a term.",199        SPACES.len(),200        keys(Tier::Closed).len(),201        keys(Tier::Convolved).len()202    )203}204205fn fill(tag: &str) -> String {206    let record = |id: &str| record_by_id(id).expect("the placeholder names a record");207    match tag.split_once(' ') {208        Some(("terms", id)) => format!("`{}`", record(id).terms),209        Some(("formula", id)) => format!("`{}`, offset {}", record(id).formula, record(id).offset),210        _ => match tag {211            "sides" => table(Axis::Side),212            "levels" => table(Axis::Level),213            "cited" => cited(),214            "records" => records(),215            "tally" => tally(),216            other => panic!("unknown placeholder {other}"),217        },218    }219}220221/// Renders the sequences page: the prose with every generated table and number filled in.222pub fn markdown() -> String {223    let mut out = String::new();224    let mut rest = PROSE;225    while let Some(open) = rest.find("{{") {226        out.push_str(&rest[..open]);227        let close = open + rest[open..].find("}}").expect("every placeholder closes");228        out.push_str(&fill(rest[open + 2..close].trim()));229        rest = &rest[close + 2..];230    }231    out.push_str(rest);232    out233}234235#[cfg(test)]236mod tests {237    use super::*;238    use std::fs;239    use std::path::Path;240241    #[test]242    fn the_page_on_disk_is_the_rendered_page() {243        let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../research/sequences.md");244        let disk = fs::read_to_string(&path).expect("the page is on disk");245        let page = markdown();246        assert!(!page.contains("{{"));247        assert!(page.contains(248            "| 8a9e4ce8 | carpet | `sequence_dim=2_code=7_measure=fills_axis=side` | `3k^2 - 2k` | `8, 21, 40, 65, 96, 133, 176, 225`"249        ));250        assert_eq!(page, disk, "run `cargo run -p mrlylab --bin ledger`");251    }252}