use std::{ collections::{BTreeMap, VecDeque}, str::FromStr, sync::OnceLock, }; use regex::Regex; #[derive(Debug, PartialEq, Eq)] pub struct Verse { // pub background: String, // url pub content: String, } impl Verse { fn new(content: String) -> Self { Self { content } } } /// Sequence of verse names. pub type Plan = VecDeque; pub struct Song { pub name: String, pub verses: BTreeMap, pub other_plans: BTreeMap, pub default_plan: Plan, } impl Song { pub fn plan(&self, plan_name: Option<&str>) -> &VecDeque { plan_name .map(|plan_name| { self.other_plans .get(plan_name) .unwrap_or(&self.default_plan) }) .unwrap_or(&self.default_plan) } } #[derive(Debug)] pub struct SourceRef { line_number: usize, } #[derive(Debug)] pub enum SongParseError { EmptyString, InvalidMetadata(SourceRef), } impl FromStr for Song { type Err = SongParseError; fn from_str(s: &str) -> Result { if s == "" { return Err(SongParseError::EmptyString); } // TODO: some way to encode comments in a song struct so that if/when we // serialize it back into a string format they are preserved? // would probably best be done with an actual AST static COMMENT_REGEX: OnceLock = OnceLock::new(); let comment_re = COMMENT_REGEX.get_or_init(|| Regex::new(r"(?s)#[^\n]*").unwrap()); let s = comment_re.replace_all(s, "").into_owned(); dbg!(&s); static HUNK_REGEX: OnceLock = OnceLock::new(); let hunk_re = HUNK_REGEX.get_or_init(|| Regex::new(r"\s*[\n\r]\s*[\n\r]\s*").unwrap()); let mut hunks = VecDeque::new(); let mut last_end: usize = 0; for m in hunk_re.find_iter(&s) { hunks.push_back(s[last_end..m.start()].trim()); last_end = m.end(); } hunks.push_back(s[last_end..s.len()].trim()); // process header let mut header_lines = hunks.pop_front().unwrap().lines().map(|s| s.trim()); let name = header_lines.next().unwrap().trim().to_owned(); let mut other_plans = BTreeMap::new(); for (line_number, line) in header_lines.enumerate() { if line.starts_with("plan(") { if let Some(end) = line.find(")") { match line[end..].find(":") { Some(i) => { let plan_name = &line[5..end]; let entries: VecDeque = line[(end + i + 1)..] .trim() .split(',') .map(|s| s.trim().to_owned()) .collect(); other_plans.insert(plan_name.to_owned(), entries); } None => { return Err(SongParseError::InvalidMetadata(SourceRef { line_number })); } } } } // map(band2): slide1, slide2 // band2: slide1, slide2 } let mut verses = BTreeMap::new(); let mut default_plan = Plan::new(); // process verses for hunk in hunks { if hunk.starts_with('(') { if hunk.ends_with(')') && !hunk.contains('\n') { default_plan.push_back(hunk[1..hunk.len() - 1].to_owned()); continue; } } let mut verse_contents: &str = hunk; let end_i = hunk.find('\n').unwrap_or(hunk.len()); let verse_name: String = if let Some(i) = &hunk[0..end_i].find(':') { verse_contents = &&hunk[end_i + 1..]; String::from(&hunk[0..*i]) } else { format!("Generated Verse {}", verses.len() + 1).to_owned() }; verses.insert(verse_name.clone(), Verse::new(verse_contents.to_owned())); default_plan.push_back(verse_name.clone()); } Ok(Self { name, verses, other_plans, default_plan, }) } } mod test { use super::*; use std::collections::VecDeque; #[test] fn parses_simple_song() { let song: Song = r#"Song Title A verse"# .parse() .unwrap(); assert_eq!(song.name, "Song Title"); assert_eq!( song.verses.get("Generated Verse 1"), Some(&Verse { content: "A verse".to_owned() }) ); assert_eq!(song.verses.len(), 1); assert_eq!(song.default_plan[0], "Generated Verse 1"); assert_eq!(song.default_plan.len(), 1); } #[test] fn parses_song_with_comments() { let song: Song = r#"Song Title # this is a comment A verse"# .parse() .unwrap(); assert_eq!(song.name, "Song Title"); assert_eq!( song.verses.get("Generated Verse 1"), Some(&Verse { content: "A verse".to_owned() }) ); assert_eq!(song.verses.len(), 1); assert_eq!(song.default_plan[0], "Generated Verse 1"); assert_eq!(song.default_plan.len(), 1); } #[test] fn parses_song_with_plan() { let song: Song = r#"Song Title plan(another_plan): Generated Verse 1, Generated Verse 1, Generated Verse 1 A verse"# .parse() .unwrap(); assert_eq!(song.name, "Song Title"); assert_eq!( song.verses.get("Generated Verse 1"), Some(&Verse { content: "A verse".to_owned() }) ); assert_eq!(song.verses.len(), 1); assert_eq!(song.default_plan[0], "Generated Verse 1"); assert_eq!(song.default_plan.len(), 1); dbg!(&song.other_plans); assert_eq!( song.other_plans.get("another_plan"), Some(&VecDeque::from(vec![ "Generated Verse 1".to_owned(), "Generated Verse 1".to_owned(), "Generated Verse 1".to_owned() ])) ); } #[test] fn parses_song_with_verse_ref() { let song: Song = r#"Title v1: v1content v2: v2 (v2) (v1)"# .parse() .unwrap(); assert_eq!(song.name, "Title"); assert_eq!( song.verses.get("v1"), Some(&Verse { content: "v1content".to_owned() }) ); assert_eq!(song.verses.len(), 2); assert_eq!(song.default_plan[0], "v1"); assert_eq!(song.default_plan.len(), 4); dbg!(&song.other_plans); assert_eq!( song.default_plan, VecDeque::from(vec![ "v1".to_owned(), "v2".to_owned(), "v2".to_owned(), "v1".to_owned(), ]) ); } }