diff --git a/src/model.rs b/src/model.rs index 1f81562..0c17167 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,208 +1,2 @@ -mod song { - 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) -> &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); - } - - static HUNK_REGEX: OnceLock = OnceLock::new(); - let 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 re.find_iter(s) { - hunks.push_back(&s[last_end..m.start()]); - last_end = m.end(); - } - hunks.push_back(&s[last_end..s.len()]); - - // 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 { - let mut verse_contents = 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 std::collections::VecDeque; - - use super::{Song, Verse}; - - #[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_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() - ])) - ); - } - } -} - -mod display { - use super::song::{Plan, Song}; - - pub struct PlaylistEntry { - pub song: Song, - pub map: Plan, - } - - pub struct PlaylistVerseRef { - pub song_index: usize, - pub song_map: String, - pub map_verse_index: usize, - } - - pub struct Display { - pub playlist: Vec<(Song, Option)>, - pub current: PlaylistVerseRef, - pub frozen_at: Option, - pub blanked: bool, - } -} +mod display; +mod song; diff --git a/src/model/display.rs b/src/model/display.rs new file mode 100644 index 0000000..450e8e7 --- /dev/null +++ b/src/model/display.rs @@ -0,0 +1,19 @@ +use super::song::{Plan, Song}; + +pub struct PlaylistEntry { + pub song: Song, + pub map: Plan, +} + +pub struct PlaylistVerseRef { + pub song_index: usize, + pub song_map: String, + pub map_verse_index: usize, +} + +pub struct Display { + pub playlist: Vec<(Song, Option)>, + pub current: PlaylistVerseRef, + pub frozen_at: Option, + pub blanked: bool, +} diff --git a/src/model/song.rs b/src/model/song.rs new file mode 100644 index 0000000..42f8d47 --- /dev/null +++ b/src/model/song.rs @@ -0,0 +1,222 @@ +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) -> &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); + } + + static HUNK_REGEX: OnceLock = OnceLock::new(); + let 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 re.find_iter(s) { + hunks.push_back(&s[last_end..m.start()]); + last_end = m.end(); + } + hunks.push_back(&s[last_end..s.len()]); + + // 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 { + let mut verse_contents = 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 std::collections::VecDeque; + + use super::{Song, Verse}; + + #[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_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: + v1 + + + v2: + v2 + + (v2) + + (v1)"# + .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() + ])) + ); + } +}