chordlib 0.15.0

Work with chord-and-lyrics songs: parse, transform, and render them to multiple formats
Documentation
use super::render_part;
use crate::types::{ChordRepresentation, Line, Part, SimpleChord};

type NextPartInfo<'a> = (&'a Part, Option<usize>, Option<usize>, bool, bool);

struct LineRenderer<'a> {
    parts: std::slice::Iter<'a, Part>,
    next_part: Option<NextPartInfo<'a>>,
    inside_word: bool,
    key: &'a SimpleChord,
    representation: &'a ChordRepresentation,
    language: usize,
}

impl<'a> Iterator for LineRenderer<'a> {
    type Item = (String, String);

    fn next(&mut self) -> Option<Self::Item> {
        let (part, first, last, _, ends_with) = self.next_part?;
        self.update_next_part();

        let next_starts_with = self
            .next_part
            .is_some_and(|(_, _, _, starts_with, _)| starts_with);

        let inside_word = !ends_with && !next_starts_with && self.next_part.is_some();

        let end = if self.inside_word && first.is_some() {
            first
        } else {
            None
        };
        let start = if inside_word && last.is_some() {
            last
        } else {
            None
        };

        self.inside_word = inside_word;

        let (part, text_chars, chord_chars) = render_part::render_part(
            part,
            self.key,
            self.representation,
            self.language,
            start,
            end,
        );

        Some((
            part,
            Self::suffix(
                self.next_part.is_some(),
                inside_word,
                chord_chars as i32 - text_chars as i32 + 1,
            ),
        ))
    }
}

impl<'a> LineRenderer<'a> {
    fn new(
        line: &'a Line,
        key: &'a SimpleChord,
        representation: &'a ChordRepresentation,
        language: usize,
    ) -> Self {
        let mut s = Self {
            parts: line.parts.iter(),
            next_part: None,
            inside_word: false,
            key,
            representation,
            language,
        };
        s.update_next_part();
        s
    }

    fn update_next_part(&mut self) {
        self.next_part = self.parts.next().map(|p| {
            let next_text = p.text_for_language(self.language);
            let mut first = None;
            let mut last = None;
            let mut starts_with = false;
            let mut ends_with = false;

            for (i, c) in next_text.char_indices() {
                if c.is_whitespace() {
                    if first.is_none() {
                        first = Some(i);
                    }
                    last = Some(i);
                    ends_with = true;
                    if i == 0 {
                        starts_with = true;
                    }
                } else {
                    ends_with = false;
                }
            }

            (p, first, last, starts_with, ends_with)
        });
    }

    fn suffix(has_next: bool, inside_word: bool, diff: i32) -> String {
        if !has_next || diff < 1 {
            return "".to_string();
        }
        let diff = diff as usize;
        let mut result = String::with_capacity(64 + diff * 6);
        result.push_str("<span class=\"text\">");
        if inside_word {
            let spaces = (diff - 1) * 2;
            for _ in 0..spaces {
                result.push(' ');
            }
            result.push('-');
            for _ in 0..spaces {
                result.push(' ');
            }
        } else {
            let spaces = diff * 5;
            for _ in 0..spaces {
                result.push(' ');
            }
        }
        result.push_str("</span>");
        result
    }

    fn render(&mut self) -> String {
        let mut result = String::with_capacity(256);
        for (part, suffix) in self.by_ref() {
            result.push_str(&part);
            result.push_str(&suffix);
        }
        result.push_str("<br>");
        result
    }
}

pub fn render_line(
    line: &Line,
    key: &SimpleChord,
    representation: &ChordRepresentation,
    language: usize,
) -> String {
    LineRenderer::new(line, key, representation, language).render()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::{Chord, Line, Part};
    use std::str::FromStr;

    #[test]
    fn render_line_falls_back_to_primary_language_for_missing_translation() {
        let line = Line::new(vec![
            Part {
                chord: Some(Chord::from_str("C").unwrap()),
                languages: vec!["Nurdeutsch".to_string()],
                comment: false,
            },
            Part {
                chord: None,
                languages: vec!["Auchnurdeutsch".to_string()],
                comment: false,
            },
        ]);
        let key = SimpleChord::default();
        let rep = ChordRepresentation::Default;

        let html = render_line(&line, &key, &rep, 1);

        assert!(html.contains("Nurdeutsch"));
        assert!(html.contains("Auchnurdeutsch"));
        assert!(
            !html.contains("<span class=\"text\"> </span>"),
            "missing translation must not render empty text placeholder: {html}"
        );
    }

    #[test]
    fn render_line_uses_translation_when_present() {
        let line = Line::new(vec![Part {
            chord: Some(Chord::from_str("C").unwrap()),
            languages: vec!["Nur Deutsch".to_string(), "Only English".to_string()],
            comment: false,
        }]);
        let key = SimpleChord::default();
        let rep = ChordRepresentation::Default;

        let html_lang1 = render_line(&line, &key, &rep, 1);

        assert!(html_lang1.contains("Only English"));
        assert!(!html_lang1.contains("Nur Deutsch"));
    }
}