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 {
|
||||
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()
|
||||
]))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
mod display;
|
||||
mod song;
|
||||
|
|
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