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}