diff --git a/src/model/song.rs b/src/model/song.rs index 42f8d47..58c26f1 100644 --- a/src/model/song.rs +++ b/src/model/song.rs @@ -59,16 +59,28 @@ impl FromStr for Song { 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 re = HUNK_REGEX.get_or_init(|| Regex::new(r"\s*[\n\r]\s*[\n\r]\s*").unwrap()); + 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 re.find_iter(s) { - hunks.push_back(&s[last_end..m.start()]); + 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()]); + hunks.push_back(s[last_end..s.len()].trim()); // process header let mut header_lines = hunks.pop_front().unwrap().lines().map(|s| s.trim()); @@ -104,10 +116,16 @@ impl FromStr for Song { // process verses for hunk in hunks { - let mut verse_contents = hunk; + 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..]; + verse_contents = &&hunk[end_i + 1..]; String::from(&hunk[0..*i]) } else { format!("Generated Verse {}", verses.len() + 1).to_owned() @@ -134,7 +152,27 @@ mod test { fn parses_simple_song() { let song: Song = r#"Song Title - A verse"# +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(); @@ -153,9 +191,9 @@ mod test { #[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"# +plan(another_plan): Generated Verse 1, Generated Verse 1, Generated Verse 1 + +A verse"# .parse() .unwrap(); @@ -186,37 +224,38 @@ mod test { - v1: - v1 - +v1: +v1content - v2: - v2 - - (v2) - - (v1)"# + +v2: +v2 + +(v2) + +(v1)"# .parse() .unwrap(); - assert_eq!(song.name, "Song Title"); + assert_eq!(song.name, "Title"); assert_eq!( - song.verses.get("Generated Verse 1"), + song.verses.get("v1"), Some(&Verse { - content: "A verse".to_owned() + content: "v1content".to_owned() }) ); - assert_eq!(song.verses.len(), 1); - assert_eq!(song.default_plan[0], "Generated Verse 1"); - assert_eq!(song.default_plan.len(), 1); + 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.other_plans.get("another_plan"), - Some(&VecDeque::from(vec![ - "Generated Verse 1".to_owned(), - "Generated Verse 1".to_owned(), - "Generated Verse 1".to_owned() - ])) + song.default_plan, + VecDeque::from(vec![ + "v1".to_owned(), + "v2".to_owned(), + "v2".to_owned(), + "v1".to_owned(), + ]) ); } }