chordlib 0.8.1

Work with chord-and-lyrics songs: parse, transform, and render them to multiple formats
Documentation
use super::{FormatOutputLines, OutputLine};
use crate::types::{ChordRepresentation, SimpleChord, Song};
use std::cmp::max;
use std::iter::{Chain, Repeat, Take, Zip};
use std::slice::Iter;

#[derive(Default)]
pub struct CharPageSet<'a> {
    keyword_prefix: &'a str,
    keyword_suffix: &'a str,
    text_prefix: &'a str,
    text_suffix: &'a str,
    chord_prefix: &'a str,
    chord_suffix: &'a str,
}

impl<'a> CharPageSet<'a> {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn keyword_prefix(mut self, keyword_prefix: &'a str) -> Self {
        self.keyword_prefix = keyword_prefix;
        self
    }
    pub fn keyword_suffix(mut self, keyword_suffix: &'a str) -> Self {
        self.keyword_suffix = keyword_suffix;
        self
    }
    pub fn text_prefix(mut self, text_prefix: &'a str) -> Self {
        self.text_prefix = text_prefix;
        self
    }
    pub fn text_suffix(mut self, text_suffix: &'a str) -> Self {
        self.text_suffix = text_suffix;
        self
    }
    pub fn chord_prefix(mut self, chord_prefix: &'a str) -> Self {
        self.chord_prefix = chord_prefix;
        self
    }
    pub fn chord_suffix(mut self, chord_suffix: &'a str) -> Self {
        self.chord_suffix = chord_suffix;
        self
    }
}

#[derive(Debug, Clone)]
pub enum CharPageLine {
    Keyword(String),
    Chord(String),
    Text(String),
    Empty,
}

impl CharPageLine {
    pub fn len(&self) -> usize {
        match self {
            Self::Keyword(s) => s.chars().count() + 1,
            Self::Chord(s) => s.chars().count(),
            Self::Text(s) => s.chars().count(),
            Self::Empty => 0,
        }
    }

    pub fn render(&self, set: &CharPageSet) -> String {
        match self {
            CharPageLine::Keyword(s) => {
                format!("{}{}:{}", set.keyword_prefix, s, set.keyword_suffix)
            }
            CharPageLine::Text(s) => format!("{}{}{}", set.text_prefix, s, set.text_suffix),
            CharPageLine::Chord(s) => format!("{}{}{}", set.chord_prefix, s, set.chord_suffix),
            CharPageLine::Empty => "".into(),
        }
    }
}

#[derive(Debug, Default)]
pub struct CharPage {
    first_column: Vec<CharPageLine>,
    second_column: Vec<CharPageLine>,
    first_column_width: usize,
    first_column_height: usize,
    second_column_width: usize,
    second_column_height: usize,
    max_width: usize,
    max_height: usize,
}

impl CharPage {
    pub fn new(max_width: usize, max_height: usize) -> Self {
        Self {
            max_width,
            max_height,
            ..Self::default()
        }
    }

    #[allow(clippy::type_complexity)]
    pub fn rows(
        &self,
    ) -> Take<
        Zip<
            Chain<Iter<'_, CharPageLine>, Repeat<&CharPageLine>>,
            Chain<Iter<'_, CharPageLine>, Repeat<&CharPageLine>>,
        >,
    > {
        self.first_column
            .iter()
            .chain(std::iter::repeat(&CharPageLine::Empty))
            .zip(
                self.second_column
                    .iter()
                    .chain(std::iter::repeat(&CharPageLine::Empty)),
            )
            .take(max(self.first_column_height, self.second_column_height))
    }

    pub fn render(&self, set: &CharPageSet) -> String {
        self.rows()
            .map(|(first, second)| (first.render(set), first.len(), second.render(set)))
            .map(|(first, first_len, second)| {
                format!(
                    "{}{}{}",
                    first,
                    " ".repeat(self.max_width - first_len - self.second_column_width),
                    second
                )
            })
            .collect::<Vec<String>>()
            .join("\n")
    }

    pub fn try_add_lines(&mut self, lines: Vec<CharPageLine>) -> Vec<CharPageLine> {
        let height = lines.len();
        let width = lines.iter().map(|line| line.len()).max().unwrap();
        if self.max_height - self.first_column_height > height {
            if self.first_column_height > 0 {
                self.first_column.push(CharPageLine::Empty);
                self.first_column.push(CharPageLine::Empty);
                self.first_column_height += 1;
            }
            self.first_column.extend(lines);
            self.first_column_height += height;
            self.first_column_width = max(self.first_column_width, width);
            Vec::new()
        } else if self.max_width - self.first_column_width > width
            && self.max_height - self.second_column_height > height
        {
            if self.second_column_height > 0 {
                self.second_column.push(CharPageLine::Empty);
                self.second_column.push(CharPageLine::Empty);
                self.second_column_height += 1;
            }
            self.second_column.extend(lines);
            self.second_column_height += height;
            self.second_column_width = max(self.first_column_width, width);
            Vec::new()
        } else {
            lines
        }
    }
}

pub trait FormatCharPages {
    fn format_char_pages(
        &self,
        max_width: usize,
        max_height: usize,
        key: Option<&SimpleChord>,
        representation: Option<&ChordRepresentation>,
        language: Option<usize>,
    ) -> Vec<CharPage>;
}

impl FormatCharPages for &Song {
    fn format_char_pages(
        &self,
        max_width: usize,
        max_height: usize,
        key: Option<&SimpleChord>,
        representation: Option<&ChordRepresentation>,
        language: Option<usize>,
    ) -> Vec<CharPage> {
        let mut char_pages = vec![CharPage::new(max_width, max_height)];

        for section in &self.sections {
            let lines = char_pages.last_mut().unwrap().try_add_lines(
                section
                    .format_output_lines(key, representation, language)
                    .into_iter()
                    .map(|line| match line {
                        OutputLine::Keyword(s) => CharPageLine::Keyword(s),
                        OutputLine::Chord(s) => CharPageLine::Chord(s),
                        OutputLine::Text(s) => CharPageLine::Text(s),
                    })
                    .collect(),
            );

            if !lines.is_empty() {
                let mut new_char_page = CharPage::new(max_width, max_height);
                new_char_page.try_add_lines(lines);
                char_pages.push(new_char_page);
            }
        }

        char_pages
    }
}