presenterm 0.16.1

A terminal slideshow presentation tool
use crate::{
    markdown::elements::{Line, Text},
    presentation::builder::{BuildResult, LastElement, PresentationBuilder},
    theme::{ElementType, raw::RawColor},
    ui::separator::RenderSeparator,
};

impl PresentationBuilder<'_, '_> {
    pub(crate) fn push_slide_title(&mut self, text: Vec<Line<RawColor>>) -> BuildResult {
        if self.options.implicit_slide_ends && !matches!(self.slide_state.last_element, LastElement::None) {
            self.terminate_slide();
        }

        let mut style = self.theme.slide_title.clone();
        self.push_line_breaks(style.padding_top as usize);
        for (index, title_line) in text.into_iter().enumerate() {
            let mut title_line = title_line.resolve(&self.theme.palette)?;
            self.slide_state.title.get_or_insert_with(|| title_line.clone());

            if let (prefix, 0) = (&style.prefix, index) {
                if !prefix.is_empty() {
                    let mut prefix = prefix.clone();
                    prefix.push(' ');
                    title_line.0.insert(0, Text::from(prefix));
                }
            }
            if let Some(font_size) = self.slide_state.font_size {
                style.style = style.style.size(font_size);
            }
            title_line.apply_style(&style.style);
            self.push_text(title_line, ElementType::SlideTitle);
            self.push_line_break();
        }

        for _ in 0..style.padding_bottom {
            self.push_line_break();
        }
        if style.separator {
            self.chunk_operations
                .push(RenderSeparator::new(Line::default(), Default::default(), style.style.size).into());
            self.push_line_break();
        }
        self.push_line_break();
        self.slide_state.ignore_element_line_break = true;
        Ok(())
    }

    pub(crate) fn push_heading(&mut self, level: u8, text: Line<RawColor>) -> BuildResult {
        if level == 1
            && self.options.h1_slide_titles
            && (self.slide_state.title.is_none() || self.options.implicit_slide_ends)
        {
            return self.push_slide_title(vec![text]);
        }
        let mut text = text.resolve(&self.theme.palette)?;
        let (element_type, style) = match level {
            1 => (ElementType::Heading1, &self.theme.headings.h1),
            2 => (ElementType::Heading2, &self.theme.headings.h2),
            3 => (ElementType::Heading3, &self.theme.headings.h3),
            4 => (ElementType::Heading4, &self.theme.headings.h4),
            5 => (ElementType::Heading5, &self.theme.headings.h5),
            6 => (ElementType::Heading6, &self.theme.headings.h6),
            other => panic!("unexpected heading level {other}"),
        };
        if let Some(prefix) = &style.prefix {
            if !prefix.is_empty() {
                let mut prefix = prefix.clone();
                prefix.push(' ');
                text.0.insert(0, Text::from(prefix));
            }
        }
        text.apply_style(&style.style);

        self.push_text(text, element_type);
        self.push_line_break();
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use crate::{
        markdown::text_style::Color,
        presentation::builder::{PresentationBuilderOptions, utils::Test},
        theme::raw,
    };

    #[test]
    fn slide_title() {
        let input = "
title
===

hi
";
        let color = Color::new(1, 1, 1);
        let theme = raw::PresentationTheme {
            slide_title: raw::SlideTitleStyle {
                separator: true,
                padding_top: Some(1),
                padding_bottom: Some(1),
                colors: raw::RawColors { foreground: None, background: Some(raw::RawColor::Color(color)) },
                ..Default::default()
            },
            ..Default::default()
        };
        let lines = Test::new(input).theme(theme).render().rows(8).columns(5).into_lines();
        let expected = &["     ", "     ", "title", "     ", "—————", "     ", "hi   ", "     "];
        assert_eq!(lines, expected);
    }

    #[test]
    fn slide_title_prefix() {
        let input = "---
theme:
  override:
    slide_title:
      prefix: \"#\"
---

title
===

";
        let lines = Test::new(input).render().rows(2).columns(7).into_lines();
        let expected = &["       ", "# title"];
        assert_eq!(lines, expected);
    }

    #[test]
    fn h1_slide_title() {
        let input = "---
options:
  h1_slide_titles: true
theme:
  override:
    slide_title:
      separator: true
---

# title

hi
";
        let lines = Test::new(input).render().rows(6).columns(5).into_lines();
        let expected = &["     ", "title", "—————", "     ", "hi   ", "     "];
        assert_eq!(lines, expected);
    }

    #[test]
    fn h1_slide_title_implicit_slides() {
        let input = "---
options:
  h1_slide_titles: true
  implicit_slide_ends: true
theme:
  override:
    slide_title:
      separator: true
---

# title

hi

# other

bye
";
        let lines = Test::new(input).render().rows(8).columns(5).into_lines();
        let expected = &["     ", "title", "—————", "     ", "hi   ", "     ", "     ", "     "];
        assert_eq!(lines, expected);
    }

    #[test]
    fn centered_slide_title() {
        let input = "
hi
===

";
        let theme = raw::PresentationTheme {
            slide_title: raw::SlideTitleStyle {
                alignment: Some(raw::Alignment::Center { minimum_margin: raw::Margin::Fixed(1), minimum_size: 0 }),
                ..Default::default()
            },
            ..Default::default()
        };
        let lines = Test::new(input).theme(theme).render().rows(3).columns(6).into_lines();
        let expected = &["      ", "  hi  ", "      "];
        assert_eq!(lines, expected);
    }

    #[test]
    fn implicit_slide_ends() {
        let input = "
hi
===

foo

bye
===

bar

";
        let options = PresentationBuilderOptions { implicit_slide_ends: true, ..Default::default() };
        let lines = Test::new(input).options(options).render().rows(4).columns(6).advances(1).into_lines();
        let expected = &["      ", "bye   ", "      ", "bar   "];
        assert_eq!(lines, expected);
    }

    #[test]
    fn headings() {
        let input = "
# A
## B
### C
#### D
##### E
";
        let theme = raw::PresentationTheme {
            headings: raw::HeadingStyles {
                h1: raw::HeadingStyle { prefix: Some("!".to_string()), ..Default::default() },
                h2: raw::HeadingStyle { prefix: Some("@@".to_string()), ..Default::default() },
                h3: raw::HeadingStyle {
                    alignment: Some(raw::Alignment::Center { minimum_margin: raw::Margin::Fixed(1), minimum_size: 0 }),
                    ..Default::default()
                },
                ..Default::default()
            },
            ..Default::default()
        };
        let lines = Test::new(input).theme(theme).render().rows(10).columns(6).advances(1).into_lines();
        let expected =
            &["      ", "! A   ", "      ", "@@ B  ", "      ", "  C   ", "      ", "D     ", "      ", "E     "];
        assert_eq!(lines, expected);
    }

    #[test]
    fn heading_font_size() {
        let input = "
<!-- font_size: 3 -->
# hi
<!-- font_size: 2 -->
## bye
";
        let lines = Test::new(input).render().rows(6).columns(10).into_lines();
        let expected = &[
            //
            "          ",
            "h  i      ",
            "          ",
            "          ", // text end (3 rows)
            "          ", // a single new line
            "b y e     ", // the next text
        ];
        assert_eq!(lines, expected);
    }
}