266 lines
7.2 KiB
Rust
266 lines
7.2 KiB
Rust
#![allow(dead_code)]
|
|
|
|
use std::{
|
|
collections::{BTreeMap, VecDeque},
|
|
str::FromStr,
|
|
sync::OnceLock,
|
|
};
|
|
|
|
use regex::Regex;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
#[derive(Serialize, Deserialize, 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>;
|
|
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
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<&str>) -> &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);
|
|
}
|
|
|
|
// 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)\r?\n?#[^\r\n]*").unwrap());
|
|
|
|
let s = comment_re.replace_all(s, "").into_owned();
|
|
|
|
dbg!(&s);
|
|
|
|
static HUNK_REGEX: OnceLock<Regex> = OnceLock::new();
|
|
let hunk_re = HUNK_REGEX.get_or_init(|| Regex::new(r"\s*[\r\n]\s*[\r\n]\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<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 {
|
|
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,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
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(),
|
|
])
|
|
);
|
|
}
|
|
}
|