lyrs/src/model/song.rs

262 lines
7 KiB
Rust
Raw Normal View History

2024-07-06 18:18:34 -05:00
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<String>;
pub struct Song {
pub name: String,
pub verses: BTreeMap<String, Verse>,
pub other_plans: BTreeMap<String, Plan>,
pub default_plan: Plan,
}
impl Song {
pub fn plan(&self, plan_name: Option<String>) -> &VecDeque<String> {
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<Self, Self::Err> {
if s == "" {
return Err(SongParseError::EmptyString);
}
2024-07-07 12:39:34 -05:00
// 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<Regex> = 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);
2024-07-06 18:18:34 -05:00
static HUNK_REGEX: OnceLock<Regex> = OnceLock::new();
2024-07-07 12:39:34 -05:00
let hunk_re = HUNK_REGEX.get_or_init(|| Regex::new(r"\s*[\n\r]\s*[\n\r]\s*").unwrap());
2024-07-06 18:18:34 -05:00
let mut hunks = VecDeque::new();
let mut last_end: usize = 0;
2024-07-07 12:39:34 -05:00
for m in hunk_re.find_iter(&s) {
hunks.push_back(s[last_end..m.start()].trim());
2024-07-06 18:18:34 -05:00
last_end = m.end();
}
2024-07-07 12:39:34 -05:00
hunks.push_back(s[last_end..s.len()].trim());
2024-07-06 18:18:34 -05:00
// 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<String> = 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 {
2024-07-07 12:39:34 -05:00
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;
2024-07-06 18:18:34 -05:00
let end_i = hunk.find('\n').unwrap_or(hunk.len());
let verse_name: String = if let Some(i) = &hunk[0..end_i].find(':') {
2024-07-07 12:39:34 -05:00
verse_contents = &&hunk[end_i + 1..];
2024-07-06 18:18:34 -05:00
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
2024-07-07 12:39:34 -05:00
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"#
2024-07-06 18:18:34 -05:00
.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
2024-07-07 12:39:34 -05:00
plan(another_plan): Generated Verse 1, Generated Verse 1, Generated Verse 1
A verse"#
2024-07-06 18:18:34 -05:00
.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
2024-07-07 12:39:34 -05:00
v1:
v1content
2024-07-06 18:18:34 -05:00
2024-07-07 12:39:34 -05:00
v2:
v2
(v2)
(v1)"#
2024-07-06 18:18:34 -05:00
.parse()
.unwrap();
2024-07-07 12:39:34 -05:00
assert_eq!(song.name, "Title");
2024-07-06 18:18:34 -05:00
assert_eq!(
2024-07-07 12:39:34 -05:00
song.verses.get("v1"),
2024-07-06 18:18:34 -05:00
Some(&Verse {
2024-07-07 12:39:34 -05:00
content: "v1content".to_owned()
2024-07-06 18:18:34 -05:00
})
);
2024-07-07 12:39:34 -05:00
assert_eq!(song.verses.len(), 2);
assert_eq!(song.default_plan[0], "v1");
assert_eq!(song.default_plan.len(), 4);
2024-07-06 18:18:34 -05:00
dbg!(&song.other_plans);
assert_eq!(
2024-07-07 12:39:34 -05:00
song.default_plan,
VecDeque::from(vec![
"v1".to_owned(),
"v2".to_owned(),
"v2".to_owned(),
"v1".to_owned(),
])
2024-07-06 18:18:34 -05:00
);
}
}