WIP
This commit is contained in:
parent
9bf90e32b1
commit
f487d2f23d
3 changed files with 243 additions and 208 deletions
210
src/model.rs
210
src/model.rs
|
@ -1,208 +1,2 @@
|
||||||
mod song {
|
mod display;
|
||||||
use std::{
|
mod song;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
static HUNK_REGEX: OnceLock<Regex> = 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<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 {
|
|
||||||
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<String>)>,
|
|
||||||
pub current: PlaylistVerseRef,
|
|
||||||
pub frozen_at: Option<PlaylistVerseRef>,
|
|
||||||
pub blanked: bool,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
19
src/model/display.rs
Normal file
19
src/model/display.rs
Normal file
|
@ -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<String>)>,
|
||||||
|
pub current: PlaylistVerseRef,
|
||||||
|
pub frozen_at: Option<PlaylistVerseRef>,
|
||||||
|
pub blanked: bool,
|
||||||
|
}
|
222
src/model/song.rs
Normal file
222
src/model/song.rs
Normal file
|
@ -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<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);
|
||||||
|
}
|
||||||
|
|
||||||
|
static HUNK_REGEX: OnceLock<Regex> = 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<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 {
|
||||||
|
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()
|
||||||
|
]))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue