use std::collections::BTreeMap;
use image::image_dimensions;
use serde::Serialize;
use crate::music::Notation;
use crate::prelude::*;
use crate::project::Settings;
use crate::util::{sort_lexical_by, BStr, ImgCache};
pub mod version;
mod xml;
#[derive(Serialize, Debug)]
#[serde(tag = "type")]
pub enum Block {
#[serde(rename = "b-verse")]
Verse(Verse),
#[serde(rename = "b-bullet-list")]
BulletList(BulletList),
#[serde(rename = "b-horizontal-line")]
HorizontalLine,
#[serde(rename = "b-pre")]
Pre { text: BStr },
#[serde(rename = "b-html-block")]
HtmlBlock(Inlines),
}
impl Block {
pub fn chorus_num(&self) -> Option<u32> {
if let Self::Verse(Verse {
label: VerseLabel::Chorus(num),
..
}) = self
{
*num
} else {
None
}
}
pub fn remove_chorus_num(&mut self) {
if let Self::Verse(verse) = self {
if let VerseLabel::Chorus(num) = &mut verse.label {
*num = None;
}
verse
.paragraphs
.iter_mut()
.flat_map(|p| p.iter_mut())
.for_each(Inline::remove_chorus_num);
}
}
fn verse(&self) -> Option<&Verse> {
match self {
Self::Verse(verse) => Some(verse),
_ => None,
}
}
fn verse_mut(&mut self) -> Option<&mut Verse> {
match self {
Self::Verse(verse) => Some(verse),
_ => None,
}
}
}
#[derive(Serialize, Debug)]
pub struct Inlines {
pub inlines: Box<[Inline]>,
}
impl Inlines {
pub fn new(inlines: Box<[Inline]>) -> Self {
Self { inlines }
}
fn remove_chorus_num(&mut self) {
self.inlines.iter_mut().for_each(Inline::remove_chorus_num);
}
}
impl From<Vec<Inline>> for Inlines {
fn from(inlines: Vec<Inline>) -> Self {
Self {
inlines: inlines.into(),
}
}
}
impl AsRef<[Inline]> for Inlines {
fn as_ref(&self) -> &[Inline] {
self.inlines.as_ref()
}
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "snake_case")]
#[serde(tag = "type")]
pub enum Inline {
#[serde(rename = "i-text")]
Text { text: BStr },
#[serde(rename = "i-chord")]
Chord(Chord),
#[serde(rename = "i-break")]
Break,
#[serde(rename = "i-emph")]
Emph(Inlines),
#[serde(rename = "i-strong")]
Strong(Inlines),
#[serde(rename = "i-link")]
Link(Link),
#[serde(rename = "i-image")]
Image(Image),
#[serde(rename = "i-chorus-ref")]
ChorusRef(ChorusRef),
#[serde(rename = "i-tag")]
HtmlTag(HtmlTag),
#[serde(rename = "i-transpose")]
Transpose(Transpose),
}
impl Inline {
pub fn text(text: impl Into<BStr>) -> Self {
Self::Text { text: text.into() }
}
pub fn is_break(&self) -> bool {
matches!(self, Self::Break)
}
pub fn is_xpose(&self) -> bool {
matches!(self, Self::Transpose(..))
}
pub fn unwrap_xpose(&self) -> Transpose {
match self {
Self::Transpose(xpose) => *xpose,
_ => panic!("Unexpected inline: {:?}", self),
}
}
fn remove_chorus_num(&mut self) {
match self {
Inline::Chord(c) => c.remove_chorus_num(),
Inline::Emph(e) => e.remove_chorus_num(),
Inline::Strong(s) => s.remove_chorus_num(),
Inline::ChorusRef(cr) => cr.num = None,
_ => {}
}
}
fn image(&self) -> Option<&Image> {
match self {
Self::Image(image) => Some(image),
_ => None,
}
}
fn image_mut(&mut self) -> Option<&mut Image> {
match self {
Self::Image(image) => Some(image),
_ => None,
}
}
}
#[derive(Serialize, Debug)]
pub struct Chord {
pub chord: BStr,
pub alt_chord: Option<BStr>,
pub backticks: usize,
pub baseline: bool,
pub inlines: Box<[Inline]>,
}
impl Chord {
pub fn new(
chord: BStr,
alt_chord: Option<BStr>,
backticks: usize,
baseline: bool,
inlines: Vec<Inline>,
) -> Self {
Self {
chord,
alt_chord,
backticks,
baseline,
inlines: inlines.into(),
}
}
fn remove_chorus_num(&mut self) {
self.inlines.iter_mut().for_each(Inline::remove_chorus_num);
}
}
#[derive(Serialize, Debug)]
pub struct Link {
pub url: BStr,
pub title: BStr,
pub text: BStr,
}
impl Link {
pub fn new(url: BStr, title: BStr, text: BStr) -> Self {
Self { url, title, text }
}
}
#[derive(Serialize, Debug)]
pub struct Image {
pub path: BStr,
pub title: BStr,
pub class: BStr,
pub width: u32,
pub height: u32,
#[serde(skip)]
full_path: Option<PathBuf>,
}
impl Image {
pub fn new(path: BStr, title: BStr, class: BStr) -> Self {
Self {
path,
title,
class,
width: 0,
height: 0,
full_path: None,
}
}
fn resolve(&mut self, output_dir: &Path, img_cache: &ImgCache) -> Result<()> {
let path = Path::new(&*self.path);
if self.path.contains("://") || path.is_absolute() {
bail!("Image path has to be relative and pointing to a local file.");
}
let full_path = output_dir.join(path);
let (w, h) = img_cache.try_get(&full_path, || {
image_dimensions(&full_path)
.with_context(|| format!("Couldn't read image file {:?}", full_path))
})?;
self.width = w;
self.height = h;
self.full_path = Some(full_path);
Ok(())
}
pub fn full_path(&self) -> &Path {
self.full_path.as_deref().unwrap()
}
}
#[derive(Serialize, Debug)]
pub struct ChorusRef {
pub num: Option<u32>,
pub prefix_space: BStr,
}
impl ChorusRef {
pub fn new(num: Option<u32>, prefix_space: bool) -> Self {
Self {
num,
prefix_space: if prefix_space { " ".into() } else { "".into() },
}
}
}
#[derive(Serialize, Debug)]
pub struct HtmlTag {
pub name: BStr,
pub attrs: BTreeMap<BStr, BStr>,
}
#[derive(Serialize, Clone, Copy, Debug)]
pub enum Transpose {
#[serde(rename = "t-transpose")]
Transpose(i32),
#[serde(rename = "t-notation")]
Notation(Notation),
#[serde(rename = "t-alt-transpose")]
AltTranspose(i32),
#[serde(rename = "t-alt-notation")]
AltNotation(Notation),
#[serde(rename = "t-alt-none")]
AltNone,
}
#[derive(Serialize, Clone, PartialEq, Eq, Debug)]
#[serde(rename_all = "snake_case")]
pub enum VerseLabel {
Verse(u32),
Chorus(Option<u32>),
Custom(BStr),
None {},
}
impl VerseLabel {
fn is_some(&self) -> bool {
!matches!(self, Self::None {})
}
}
pub type Paragraph = Box<[Inline]>;
#[derive(Serialize, Debug)]
pub struct Verse {
pub label: VerseLabel,
pub paragraphs: Vec<Paragraph>,
}
impl Verse {
pub fn new(label: VerseLabel, paragraphs: Vec<Paragraph>) -> Self {
Self { label, paragraphs }
}
pub fn is_empty(&self) -> bool {
self.paragraphs.is_empty()
}
fn inlines(&self) -> impl Iterator<Item = &Inline> {
self.paragraphs.iter().flat_map(|p| p.iter())
}
fn inlines_mut(&mut self) -> impl Iterator<Item = &mut Inline> {
self.paragraphs.iter_mut().flat_map(|p| p.iter_mut())
}
}
#[derive(Serialize, Debug)]
pub struct BulletList {
pub items: Box<[BStr]>,
}
#[derive(Serialize, Debug)]
pub struct Song {
pub title: BStr,
pub subtitles: Box<[BStr]>,
pub blocks: Vec<Block>,
pub notation: Notation,
}
impl Song {
pub fn postprocess(&mut self) {
for verse in self.blocks.iter_mut().filter_map(Block::verse_mut) {
verse
.paragraphs
.retain(|para| para.iter().any(|inline| !inline.is_break()));
}
self.blocks.retain(|block| match block {
Block::Verse(verse) => verse.label.is_some() || !verse.paragraphs.is_empty(),
_ => true,
});
}
}
#[derive(Serialize, Debug)]
pub struct SongRef {
pub title: BStr,
pub idx: usize,
}
impl SongRef {
pub fn new((idx, songs): (usize, &Song)) -> Self {
Self {
title: songs.title.clone(),
idx,
}
}
}
#[derive(Debug)]
pub struct Book {
pub songs: Vec<Song>,
pub songs_sorted: Vec<SongRef>,
pub notation: Notation,
}
impl Book {
pub fn new(settings: &Settings) -> Book {
Book {
songs: vec![],
songs_sorted: vec![],
notation: settings.notation,
}
}
pub fn add_songs(&mut self, mut songs: Vec<Song>) {
self.songs.reserve(songs.len());
self.songs.append(&mut songs);
}
pub fn postprocess(&mut self, output_dir: &Path, img_cache: &ImgCache) -> Result<()> {
self.songs.shrink_to_fit();
self.songs_sorted = self.songs.iter().enumerate().map(SongRef::new).collect();
sort_lexical_by(&mut self.songs_sorted, |songref| songref.title.as_ref());
for image in self.iter_images_mut() {
image.resolve(output_dir, img_cache)?;
}
Ok(())
}
pub fn iter_images(&self) -> impl Iterator<Item = &Image> {
self.songs
.iter()
.flat_map(|s| s.blocks.iter())
.filter_map(Block::verse)
.flat_map(|v| v.inlines())
.filter_map(Inline::image)
}
pub fn iter_images_mut(&mut self) -> impl Iterator<Item = &mut Image> {
self.songs
.iter_mut()
.flat_map(|s| s.blocks.iter_mut())
.filter_map(Block::verse_mut)
.flat_map(|v| v.inlines_mut())
.filter_map(Inline::image_mut)
}
}
#[cfg(test)]
pub trait AssertJsonEq {
fn assert_json_eq(&self, value: serde_json::Value);
}
#[cfg(test)]
impl<T> AssertJsonEq for T
where
T: Serialize,
{
#[track_caller]
fn assert_json_eq(&self, value: serde_json::Value) {
use assert_json_diff::{assert_json_matches_no_panic, CompareMode, Config};
let config = Config::new(CompareMode::Strict);
if let Err(diff) = assert_json_matches_no_panic(self, &value, config) {
panic!(
"JSON equality assertion failed: \n== LHS ==\n{}\n== RHS ==\n{}\n== DIFF \
==\n{}\n\n",
serde_json::to_string_pretty(self).unwrap(),
serde_json::to_string_pretty(&value).unwrap(),
diff
)
}
}
}