use std::collections::HashMap;
use std::error::Error;
use std::sync::OnceLock;
extern crate regex;
use regex::{Regex,RegexBuilder};
use crate::importer::errors::CantaraImportNoContentError;
use crate::song::{
LyricLanguage, PartOrder, PartOrderName, PartOrderRule, Song, SongPartContent, SongPartId,
SongPartType,
};
use crate::slides::*;
use crate::templating::MetaTemplate;
use crate::importer::metadata::*;
fn parse_metadata(block: &str, song: &mut Song) {
let tags_regex = {
static TAGS_REGEX: OnceLock<Regex> = OnceLock::new();
TAGS_REGEX.get_or_init(|| {
RegexBuilder::new(r"\s*#(\w+):\s*(.+)$")
.multi_line(true)
.build()
.unwrap()
})
};
for capture in tags_regex.captures_iter(block) {
let tag = capture.get(1).unwrap().as_str().to_lowercase();
let value = capture.get(2).unwrap().as_str();
song.set_tag(&tag, value);
if tag == "title" {
song.title = value.to_string();
}
}
}
struct Run {
blocks: Vec<String>,
refrain: bool,
}
fn split_into_runs(blocks: &[String]) -> Vec<Run> {
let mut occurrences: HashMap<&str, usize> = HashMap::new();
for block in blocks {
*occurrences.entry(block.as_str()).or_insert(0) += 1;
}
let is_refrain = |block: &String| occurrences[block.as_str()] > 1;
let has_refrain = blocks.iter().any(is_refrain);
let mut runs: Vec<Run> = Vec::new();
for block in blocks {
let refrain = is_refrain(block);
match runs.last_mut() {
Some(run) if run.refrain == refrain && (refrain || has_refrain) => {
run.blocks.push(block.clone())
}
_ => runs.push(Run {
blocks: vec![block.clone()],
refrain,
}),
}
}
runs
}
fn repeating_period(run: &[String]) -> usize {
(1..=run.len())
.find(|period| {
run.len().is_multiple_of(*period)
&& run
.iter()
.enumerate()
.all(|(index, block)| *block == run[index % period])
})
.unwrap_or(run.len())
}
fn split_refrain_run<'a>(run: &'a [String], known: &[Vec<String>]) -> Vec<&'a [String]> {
let mut pieces: Vec<&[String]> = Vec::new();
let mut rest = run;
while !rest.is_empty() {
let matched = known
.iter()
.filter(|refrain| rest.starts_with(refrain))
.map(|refrain| refrain.len())
.max()
.unwrap_or_else(|| repeating_period(rest));
pieces.push(&rest[..matched]);
rest = &rest[matched..];
}
pieces
}
pub fn import_song(content: &str) -> Result<Song, Box<dyn Error>> {
if content.is_empty() {
return Err(Box::new(CantaraImportNoContentError {}));
}
let title: String = match get_title_from_file_content(content) {
Some(title_string) => title_string,
None => "".to_string()
};
let mut song: Song = Song::new(&title);
let mut lyric_blocks: Vec<String> = Vec::new();
let mut block = String::new();
let finish = |block: &mut String, song: &mut Song, blocks: &mut Vec<String>| {
let text = block.trim().to_string();
block.clear();
if text.is_empty() {
return;
}
if text.starts_with('#') {
parse_metadata(&text, song);
} else {
blocks.push(text);
}
};
for line in content.trim().lines() {
if line.trim().is_empty() {
finish(&mut block, &mut song, &mut lyric_blocks);
} else {
block.push_str(line.trim());
block.push('\n');
}
}
finish(&mut block, &mut song, &mut lyric_blocks);
let runs = split_into_runs(&lyric_blocks);
let mut refrains: Vec<Vec<String>> = Vec::new();
let mut order: Vec<SongPartId> = Vec::new();
for run in &runs {
if !run.refrain {
let id = song.add_part_of_type(SongPartType::Verse, None);
song.part_mut(&id)
.unwrap()
.add_content(SongPartContent::lyrics(
LyricLanguage::Default,
run.blocks.join("\n"),
));
order.push(id);
continue;
}
for piece in split_refrain_run(&run.blocks, &refrains) {
let id = match refrains.iter().position(|refrain| refrain == piece) {
Some(index) => SongPartId::new(SongPartType::Chorus, index as u32 + 1),
None => {
refrains.push(piece.to_vec());
let id = song.add_part_of_type(SongPartType::Chorus, None);
song.part_mut(&id)
.unwrap()
.add_content(SongPartContent::lyrics(
LyricLanguage::Default,
piece.join("\n"),
));
id
}
};
order.push(id);
}
}
song.part_orders.push(order_for(&song, order));
Ok(song)
}
fn order_for(song: &Song, order: Vec<SongPartId>) -> PartOrder {
for rule in [
PartOrderRule::VerseRefrainBridgeRefrain,
PartOrderRule::RefrainVerseBridgeRefrain,
] {
let candidate = PartOrder::new(PartOrderName::Default, rule);
if candidate.to_part_ids(song) == order {
return candidate;
}
}
PartOrder::new(PartOrderName::Default, PartOrderRule::Custom(order))
}
pub fn slides_from_classic_song(
content: &str,
slide_settings: &SlideSettings,
backup_title: String) -> Vec<Slide> {
enum WritingArea {
MainBlock,
SecondaryBlock
}
let mut empty_line = false;
let mut start_block_flag = true;
let mut meta_block_flag = false;
let mut blocks: Vec<Vec<String>> = vec![];
let mut secondary_blocks: Vec<Vec<String>> = vec![];
let mut cur_block_string: String = "".to_string();
let mut cur_secundary_block_string: String = "".to_string();
let mut metadata: HashMap<String, String> = HashMap::new();
let mut writing_area: WritingArea = WritingArea::MainBlock;
fn handle_block(metadata: &mut HashMap<String, String>,
meta_block_flag: &bool,
backup_title: &str,
cur_block_string: &str,
cur_secundary_block_string: &str,
blocks: &mut Vec<Vec<String>>,
secondary_blocks: &mut Vec<Vec<String>>
) {
match meta_block_flag {
true => {
parse_metadata_block(cur_block_string)
.iter()
.for_each(|(key, value)| {
metadata.insert(key.clone(), value.clone());
});
if metadata.get("title").is_none() {
metadata.insert("title".to_string(), backup_title.to_string());
}
},
false => {
if !cur_block_string.trim().is_empty() {
blocks.push(
cur_block_string.lines()
.map(|str| str.to_string()).collect()
);
secondary_blocks.push(
cur_secundary_block_string.lines()
.map(|str| str.to_string()).collect()
);
}
},
}
}
for line in content.trim().lines() {
if empty_line { start_block_flag = true };
if start_block_flag && !line.is_empty() {
meta_block_flag = line.starts_with('#');
start_block_flag = false;
}
if line.trim().is_empty() {
empty_line = true;
writing_area = WritingArea::MainBlock;
if cur_block_string.is_empty() {
continue;
}
handle_block(&mut metadata,
&meta_block_flag,
&backup_title,
&cur_block_string,
&cur_secundary_block_string,
&mut blocks,
&mut secondary_blocks
);
cur_block_string = "".to_string();
cur_secundary_block_string = "".to_string();
}
else if line.trim() == "---" {
writing_area = WritingArea::SecondaryBlock;
}
else {
match writing_area {
WritingArea::MainBlock => {
if !cur_block_string.is_empty() {
cur_block_string.push('\n');
}
cur_block_string.push_str(line);
},
WritingArea::SecondaryBlock => {
if !cur_secundary_block_string.is_empty() {
cur_secundary_block_string.push('\n');
}
cur_secundary_block_string.push_str(line);
}
}
}
}
handle_block(&mut metadata,
&meta_block_flag,
&backup_title,
&cur_block_string,
&cur_secundary_block_string,
&mut blocks,
&mut secondary_blocks
);
if let Some(max_lines) = slide_settings.max_lines {
let wrapped = wrap_blocks(&[blocks, secondary_blocks], max_lines, true);
blocks = wrapped.first().cloned().unwrap_or_default();
secondary_blocks = wrapped.get(1).cloned().unwrap_or_default();
}
let mut slides: Vec<Slide> = vec![];
metadata
.entry("title".to_string())
.or_insert_with(|| backup_title.clone());
let meta_text: Option<String> = if slide_settings.show_meta_information.is_none() {
None
} else {
MetaTemplate::parse(&slide_settings.meta_syntax)
.ok()
.and_then(|template| template.render(&metadata))
};
if slide_settings.title_slide {
let displayed_meta_text = match slide_settings.show_meta_information.on_title_slide() {
true => meta_text.clone(),
false => None,
};
slides.push(
Slide::new_title_slide(
metadata.get("title").unwrap().into(),
displayed_meta_text
)
)
}
let count = blocks.len();
for (index, block) in blocks.iter().enumerate() {
let displayed_meta_text = match slide_settings
.show_meta_information
.on_content_slide(index, count)
{
true => meta_text.clone(),
false => None,
};
let secondary_block = secondary_blocks.get(index).unwrap();
let secondary_text = if !secondary_block.is_empty() {
Some(secondary_block.join("\n"))
} else if slide_settings.show_spoiler {
blocks.get(index + 1).map(|next_block| next_block.join("\n"))
} else {
None
};
slides.push(Slide::new_content_slide(
block.join("\n"),
secondary_text,
displayed_meta_text,
));
}
if slide_settings.empty_last_slide {
slides.push(
Slide::new_empty_slide(false)
);
}
slides
}
#[cfg(test)]
mod test {
use crate::importer::import_song_from_file;
use super::*;
#[test]
fn test_import_song() {
let content: String = String::from("#title: Test Song");
let song = import_song(&content).unwrap();
assert_eq!(song.title, "Test Song");
}
#[test]
fn test_import_song_with_tags() {
let content: String = String::from(
"#title: Test Song
#author: Test Author
#key: C"
);
let song = import_song(&content).unwrap();
assert_eq!(song.title, "Test Song");
assert_eq!(song.tag("author").unwrap(), "Test Author");
assert_eq!(song.tag("key").unwrap(), "C");
}
#[test]
fn test_import_song_with_verse() {
let content: String =
"#title: Test Song
This is a verse
And a refrain
The second verse
And a refrain"
.to_string();
let song = import_song(&content).unwrap();
assert_eq!(song.part_count_of_type(SongPartType::Verse), 2);
}
fn part_ids(song: &Song) -> Vec<String> {
song.parts()
.iter()
.map(|part| part.id().to_string())
.collect()
}
#[test]
fn test_a_song_starting_with_the_refrain_numbers_its_verses_from_one() {
let content = "#title: Alles was ihr tut
Alles was ihr tut, gescheh in Liebe,
Die Liebe ist sanftmütig und gütig.
Alles was ihr tut, gescheh in Liebe,
Jesus hat uns stets gelehrt zu lieben.
Alles was ihr tut, gescheh in Liebe,
Wir lieben nun weil Gott zuerst geliebt.";
let song = import_song(content).unwrap();
assert_eq!(
part_ids(&song),
["chorus.1", "verse.1", "verse.2", "verse.3"]
);
assert!(
song.part(&"verse.1".parse().unwrap()).is_some(),
"a song always has a first verse"
);
}
#[test]
fn test_a_song_starting_with_a_verse_keeps_its_numbering() {
let content = "#title: Test Song
This is a verse
And a refrain
The second verse
And a refrain";
let song = import_song(content).unwrap();
assert_eq!(part_ids(&song), ["verse.1", "chorus.1", "verse.2"]);
}
#[test]
fn test_the_guessed_order_refers_to_existing_parts() {
let content = "#title: Refrain First
The refrain
A verse
The refrain
Another verse";
let song = import_song(content).unwrap();
for part in song.part_orders.first().unwrap().to_parts(&song) {
let id = part.id();
assert!(
song.part(&id).is_some(),
"the order names {} which the song does not have",
id
);
}
}
#[test]
fn test_file_amazing_grace() {
let song: Song = import_song_from_file("tests/data/Amazing Grace.song").unwrap();
assert_eq!(song.title, "Amazing Grace");
assert_eq!(song.tag("author").unwrap(), "John Newton");
assert_eq!(song.part_count_of_type(SongPartType::Verse), 3)
}
#[test]
fn test_song_with_refrain() {
let song: Song = import_song_from_file("tests/data/O What A Savior That He Died For Me.song").unwrap();
assert_eq!(song.title, "O What A Savior That He Died For Me");
assert_eq!(song.part_count_of_type(SongPartType::Verse), 4);
assert_eq!(song.part_count_of_type(SongPartType::Chorus), 1);
dbg!(song);
}
#[test]
fn test_a_guessed_singing_order_is_added() {
let song: Song =
import_song_from_file("tests/data/O What A Savior That He Died For Me.song").unwrap();
assert_eq!(song.part_orders.len(), 1);
let sung: Vec<String> = song
.ordered_parts()
.iter()
.map(|part| part.id().to_string())
.collect();
assert_eq!(sung.len(), 8);
assert_eq!(
sung.iter().filter(|id| id.starts_with("chorus")).count(),
4
);
assert_eq!(sung[0], "verse.1");
assert_eq!(sung[1], "chorus.1");
}
#[test]
fn test_order_without_a_refrain() {
let song: Song = import_song_from_file("tests/data/Amazing Grace.song").unwrap();
let sung: Vec<String> = song
.ordered_parts()
.iter()
.map(|part| part.id().to_string())
.collect();
assert_eq!(sung, ["verse.1", "verse.2", "verse.3"]);
}
fn sung(song: &Song) -> Vec<String> {
song.ordered_parts()
.iter()
.map(|part| part.id().to_string())
.collect()
}
fn sung_text(song: &Song) -> Vec<String> {
song.ordered_parts()
.iter()
.flat_map(|part| {
part.contents
.iter()
.filter(|c| c.content_type.is_lyrics())
.map(|c| c.content.trim().to_string())
})
.collect()
}
#[test]
fn test_a_refrain_after_every_second_block_is_not_moved() {
let content = "#title: Vivek
Verse one, first half.
Verse one, second half.
Refrain, first half.
Refrain, second half.
Verse two, first half.
Verse two, second half.
Refrain, first half.
Refrain, second half.";
let song = import_song(content).unwrap();
assert_eq!(sung(&song), ["verse.1", "chorus.1", "verse.2", "chorus.1"]);
assert_eq!(
sung_text(&song),
[
"Verse one, first half.\nVerse one, second half.",
"Refrain, first half.\nRefrain, second half.",
"Verse two, first half.\nVerse two, second half.",
"Refrain, first half.\nRefrain, second half.",
]
);
}
#[test]
fn test_a_refrain_is_never_inserted_where_the_file_has_none() {
let content = "#title: Late Refrain
Verse one.
The refrain.
Verse two.
Verse three.
The refrain.";
let song = import_song(content).unwrap();
assert_eq!(
sung(&song),
["verse.1", "chorus.1", "verse.2", "chorus.1"],
"verses two and three are one run between the refrains"
);
}
#[test]
fn test_a_regular_song_uses_the_standard_rule() {
let content = "#title: Regular
Verse one.
The refrain.
Verse two.
The refrain.";
let song = import_song(content).unwrap();
assert_eq!(
song.part_orders[0].rule(),
&crate::song::PartOrderRule::VerseRefrainBridgeRefrain
);
assert_eq!(sung(&song), ["verse.1", "chorus.1", "verse.2", "chorus.1"]);
}
#[test]
fn test_an_irregular_song_gets_a_custom_order() {
let content = "#title: Irregular
Verse one.
The refrain.
Verse two.
The refrain.
Verse three.";
let song = import_song(content).unwrap();
assert!(
matches!(
song.part_orders[0].rule(),
crate::song::PartOrderRule::Custom(_)
),
"expected an explicit order, got {:?}",
song.part_orders[0].rule()
);
assert_eq!(
sung(&song),
["verse.1", "chorus.1", "verse.2", "chorus.1", "verse.3"],
"the song must not gain a closing refrain"
);
}
#[test]
fn test_a_two_block_refrain_is_a_single_part() {
let content = "#title: Two Block Refrain
Verse one.
Refrain A.
Refrain B.
Verse two.
Refrain A.
Refrain B.";
let song = import_song(content).unwrap();
assert_eq!(song.part_count_of_type(SongPartType::Chorus), 1);
assert!(
sung_text(&song).contains(&"Refrain A.\nRefrain B.".to_string()),
"{:?}",
sung_text(&song)
);
let all = sung_text(&song).join("\n");
assert_eq!(all.matches("Refrain B.").count(), 2, "{}", all);
}
#[test]
fn test_verses_are_not_merged_without_a_refrain() {
let song: Song = import_song_from_file("tests/data/Amazing Grace.song").unwrap();
assert_eq!(song.part_count_of_type(SongPartType::Verse), 3);
}
#[test]
fn test_a_refrain_repeated_back_to_back_is_split() {
let content = "#title: Doubled
Refrain A.
Refrain B.
Refrain A.
Refrain B.
A verse.
Refrain A.
Refrain B.";
let song = import_song(content).unwrap();
assert_eq!(song.part_count_of_type(SongPartType::Chorus), 1);
assert_eq!(
sung(&song),
["chorus.1", "chorus.1", "verse.1", "chorus.1"]
);
}
#[test]
fn generate_slides() {
let testfile = std::fs::read_to_string("tests/data/O What A Savior That He Died For Me.song").unwrap();
let presentation_settings = SlideSettings {
title_slide: true,
meta_syntax: "{{title}} ({{author}})".to_string(),
show_meta_information: ShowMetaInformation::all(),
empty_last_slide: true,
show_spoiler: true ,
max_lines: Some(10),
language: crate::slides::LanguageConfiguration::default(),
};
let slides: Vec<Slide> = slides_from_classic_song(
&testfile,
&presentation_settings,
"Verily, Verily".to_string()
);
assert!(!slides.is_empty());
dbg!(slides);
}
#[test]
fn test_metadata_displayed_correctly() {
let testfile = std::fs::read_to_string("tests/data/O What A Savior That He Died For Me.song").unwrap();
let mut presentation_settings = SlideSettings {
title_slide: false,
meta_syntax: "{{title}} ({{author}})".to_string(),
show_meta_information: ShowMetaInformation::none(),
empty_last_slide: true,
show_spoiler: true,
max_lines: None,
language: crate::slides::LanguageConfiguration::default(),
};
let slides: Vec<Slide> = slides_from_classic_song(
&testfile,
&presentation_settings,
"Verily, Verily".to_string()
);
slides.iter().for_each(|slide| assert!(!slide.has_meta_text()));
presentation_settings.show_meta_information = ShowMetaInformation::first_slide();
let slides: Vec<Slide> = slides_from_classic_song(
&testfile,
&presentation_settings,
"Verily, Verily".to_string()
);
assert!(slides[0].has_meta_text(), "the first slide should carry it");
for slide in &slides[1..] {
assert!(!slide.has_meta_text(), "only the first slide should carry it");
}
}
#[test]
fn test_metadata_on_the_last_slide() {
let testfile =
std::fs::read_to_string("tests/data/O What A Savior That He Died For Me.song").unwrap();
let settings = SlideSettings {
title_slide: false,
meta_syntax: "{{title}} ({{author}})".to_string(),
show_meta_information: ShowMetaInformation::last_slide(),
empty_last_slide: true,
show_spoiler: true,
max_lines: None,
language: crate::slides::LanguageConfiguration::default(),
};
let slides = slides_from_classic_song(&testfile, &settings, "Verily".to_string());
let carrying: Vec<usize> = slides
.iter()
.enumerate()
.filter(|(_, slide)| slide.has_meta_text())
.map(|(index, _)| index)
.collect();
assert_eq!(carrying, [slides.len() - 2]);
}
#[test]
fn test_title_slide_is_a_separate_position() {
let testfile =
std::fs::read_to_string("tests/data/O What A Savior That He Died For Me.song").unwrap();
let mut settings = SlideSettings {
title_slide: true,
meta_syntax: "{{title}} ({{author}})".to_string(),
show_meta_information: ShowMetaInformation::first_slide(),
empty_last_slide: false,
show_spoiler: true,
max_lines: None,
language: crate::slides::LanguageConfiguration::default(),
};
let slides = slides_from_classic_song(&testfile, &settings, "Verily".to_string());
assert!(!slides[0].has_meta_text(), "the title slide was not asked for");
assert!(slides[1].has_meta_text(), "the first content slide was");
settings.show_meta_information = ShowMetaInformation::title_slide();
let slides = slides_from_classic_song(&testfile, &settings, "Verily".to_string());
assert!(slides[0].has_meta_text(), "the title slide was asked for");
for slide in &slides[1..] {
assert!(!slide.has_meta_text(), "no content slide was");
}
}
#[test]
fn test_title_fallback_reaches_the_template() {
let testfile =
std::fs::read_to_string("tests/data/What a friend we have in Jesus.song").unwrap();
let settings = SlideSettings {
title_slide: true,
meta_syntax: "{{title}}".to_string(),
show_meta_information: ShowMetaInformation::title_slide(),
empty_last_slide: false,
show_spoiler: false,
max_lines: None,
language: crate::slides::LanguageConfiguration::default(),
};
let slides = slides_from_classic_song(
&testfile,
&settings,
"What a friend we have in Jesus".to_string(),
);
let rendered = serde_json::to_string(&slides[0]).unwrap();
assert!(
rendered.contains("What a friend we have in Jesus"),
"the fallback title is missing from the meta line: {}",
rendered
);
}
fn main_text_line_counts(slides: &[Slide]) -> Vec<usize> {
slides
.iter()
.filter_map(|slide| match &slide.slide_content {
SlideContent::SingleLanguageMainContent(content) => {
Some(content.clone().main_text().lines().count())
}
_ => None,
})
.collect()
}
fn wrapping_settings(max_lines: Option<usize>) -> SlideSettings {
SlideSettings {
title_slide: false,
meta_syntax: "".to_string(),
show_meta_information: ShowMetaInformation::none(),
empty_last_slide: false,
show_spoiler: false,
max_lines,
language: crate::slides::LanguageConfiguration::default(),
}
}
#[test]
fn test_max_lines_is_not_spent_on_a_leading_empty_line() {
let testfile =
std::fs::read_to_string("tests/data/O What A Savior That He Died For Me.song").unwrap();
let unwrapped = slides_from_classic_song(
&testfile,
&wrapping_settings(None),
"Verily".to_string(),
);
assert_eq!(main_text_line_counts(&unwrapped), vec![4; 8]);
let wrapped = slides_from_classic_song(
&testfile,
&wrapping_settings(Some(3)),
"Verily".to_string(),
);
assert_eq!(
main_text_line_counts(&wrapped),
[3, 1].repeat(8),
"a four-line block under a limit of three must fill the first slide"
);
}
#[test]
fn test_block_of_exactly_max_lines_is_not_wrapped() {
let testfile =
std::fs::read_to_string("tests/data/O What A Savior That He Died For Me.song").unwrap();
let slides = slides_from_classic_song(
&testfile,
&wrapping_settings(Some(4)),
"Verily".to_string(),
);
assert_eq!(main_text_line_counts(&slides), vec![4; 8]);
}
fn main_and_secondary(slides: &[Slide]) -> Vec<(Vec<String>, Vec<String>)> {
slides
.iter()
.filter_map(|slide| match &slide.slide_content {
SlideContent::SingleLanguageMainContent(content) => {
let lines = |text: String| {
text.lines().map(|line| line.to_string()).collect::<Vec<_>>()
};
Some((
lines(content.clone().main_text()),
content.clone().spoiler_text().map(lines).unwrap_or_default(),
))
}
_ => None,
})
.collect()
}
#[test]
fn test_secondary_blocks_wrap_in_step_with_the_main_block() {
let testfile = std::fs::read_to_string("tests/data/Bilingual Test Song.song").unwrap();
let slides = slides_from_classic_song(
&testfile,
&wrapping_settings(Some(3)),
"Bilingual Test Song".to_string(),
);
assert_eq!(
main_and_secondary(&slides),
vec![
(
vec![
"Main verse one line one".to_string(),
"Main verse one line two".to_string(),
"Main verse one line three".to_string(),
],
vec![
"Neben Strophe eins Zeile eins".to_string(),
"Neben Strophe eins Zeile zwei".to_string(),
"Neben Strophe eins Zeile drei".to_string(),
],
),
(
vec!["Main verse one line four".to_string()],
vec!["Neben Strophe eins Zeile vier".to_string()],
),
(
vec![
"Main verse two line one".to_string(),
"Main verse two line two".to_string(),
"Main verse two line three".to_string(),
],
vec![
"Neben Strophe zwei Zeile eins".to_string(),
"Neben Strophe zwei Zeile zwei".to_string(),
"Neben Strophe zwei Zeile drei".to_string(),
],
),
(
vec!["Main verse two line four".to_string()],
vec!["Neben Strophe zwei Zeile vier".to_string()],
),
],
);
}
#[test]
fn test_secondary_block_is_not_wrapped_without_max_lines() {
let testfile = std::fs::read_to_string("tests/data/Bilingual Test Song.song").unwrap();
let slides = slides_from_classic_song(
&testfile,
&wrapping_settings(None),
"Bilingual Test Song".to_string(),
);
let pairs = main_and_secondary(&slides);
assert_eq!(pairs.len(), 2, "one slide per stanza");
for (main, secondary) in pairs {
assert_eq!(main.len(), 4);
assert_eq!(secondary.len(), 4);
}
}
fn spoilers(slides: &[Slide]) -> Vec<Option<String>> {
slides
.iter()
.filter_map(|slide| match &slide.slide_content {
SlideContent::SingleLanguageMainContent(content) => {
Some(content.clone().spoiler_text())
}
_ => None,
})
.collect()
}
fn spoiler_settings(show_spoiler: bool) -> SlideSettings {
SlideSettings {
show_spoiler,
..wrapping_settings(None)
}
}
#[test]
fn test_show_spoiler_does_not_suppress_the_second_language() {
let testfile = std::fs::read_to_string("tests/data/Bilingual Test Song.song").unwrap();
let slides = slides_from_classic_song(
&testfile,
&spoiler_settings(false),
"Bilingual Test Song".to_string(),
);
for spoiler in spoilers(&slides) {
let spoiler = spoiler.expect("the translation is not a spoiler and must stay");
assert!(
spoiler.starts_with("Neben Strophe"),
"expected the translation, got {:?}",
spoiler
);
}
}
#[test]
fn test_show_spoiler_suppresses_the_preview_of_the_next_stanza() {
let testfile =
std::fs::read_to_string("tests/data/O What A Savior That He Died For Me.song").unwrap();
let shown = spoilers(&slides_from_classic_song(
&testfile,
&spoiler_settings(true),
"Verily".to_string(),
));
assert!(
shown[..shown.len() - 1].iter().all(|spoiler| spoiler.is_some()),
"previews are on: {:?}",
shown
);
assert_eq!(shown.last().unwrap(), &None, "the last stanza has no successor");
let hidden = spoilers(&slides_from_classic_song(
&testfile,
&spoiler_settings(false),
"Verily".to_string(),
));
assert!(
hidden.iter().all(|spoiler| spoiler.is_none()),
"previews are off: {:?}",
hidden
);
}
}