presenterm 0.16.1

A terminal slideshow presentation tool
use crate::{
    markdown::elements::{Percent, PercentParseError, SourcePosition},
    presentation::builder::{
        BuildResult, PresentationBuilder,
        error::{BuildError, InvalidPresentation},
    },
    render::operation::{ImageRenderProperties, ImageSize, RenderOperation},
    terminal::image::Image,
};
use std::path::PathBuf;

impl PresentationBuilder<'_, '_> {
    pub(crate) fn push_image_from_path(
        &mut self,
        path: PathBuf,
        title: String,
        source_position: SourcePosition,
    ) -> BuildResult {
        let base_path = self.resource_base_path();
        let image = self.resources.image(&path, &base_path).map_err(|e| {
            self.invalid_presentation(source_position, InvalidPresentation::LoadImage { path, error: e.to_string() })
        })?;
        self.push_image(image, title, source_position)
    }

    pub(crate) fn push_image(&mut self, image: Image, title: String, source_position: SourcePosition) -> BuildResult {
        let attributes = self.parse_image_attributes(&title, &self.options.image_attribute_prefix, source_position)?;
        let size = match attributes.width {
            Some(percent) => ImageSize::WidthScaled { ratio: percent.as_ratio() },
            None => ImageSize::ShrinkIfNeeded,
        };
        let properties = ImageRenderProperties {
            size,
            background_color: self.theme.default_style.style.colors.background,
            ..Default::default()
        };
        self.chunk_operations.push(RenderOperation::RenderImage(image, properties));
        Ok(())
    }

    fn parse_image_attributes(
        &self,
        input: &str,
        attribute_prefix: &str,
        source_position: SourcePosition,
    ) -> Result<ImageAttributes, BuildError> {
        let mut attributes = ImageAttributes::default();
        for attribute in input.split(',') {
            let Some((prefix, suffix)) = attribute.split_once(attribute_prefix) else { continue };
            if !prefix.is_empty() || (attribute_prefix.is_empty() && suffix.is_empty()) {
                continue;
            }
            Self::parse_image_attribute(suffix, &mut attributes)
                .map_err(|e| self.invalid_presentation(source_position, e))?;
        }
        Ok(attributes)
    }

    fn parse_image_attribute(input: &str, attributes: &mut ImageAttributes) -> Result<(), ImageAttributeError> {
        let Some((key, value)) = input.split_once(':') else {
            return Err(ImageAttributeError::AttributeMissing);
        };
        match key {
            "width" | "w" => {
                let width = value.parse().map_err(ImageAttributeError::InvalidWidth)?;
                attributes.width = Some(width);
                Ok(())
            }
            _ => Err(ImageAttributeError::UnknownAttribute(key.to_string())),
        }
    }
}

#[derive(thiserror::Error, Debug)]
pub(crate) enum ImageAttributeError {
    #[error("invalid width: {0}")]
    InvalidWidth(PercentParseError),

    #[error("no attribute given")]
    AttributeMissing,

    #[error("unknown attribute: '{0}'")]
    UnknownAttribute(String),
}

#[derive(Clone, Debug, Default, PartialEq)]
struct ImageAttributes {
    width: Option<Percent>,
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::presentation::builder::utils::Test;
    use rstest::rstest;

    #[rstest]
    #[case::width("image:width:50%", Some(50))]
    #[case::w("image:w:50%", Some(50))]
    #[case::nothing("", None)]
    #[case::no_prefix("width", None)]
    fn image_attributes(#[case] input: &str, #[case] expectation: Option<u8>) {
        let attributes = Test::new("").with_builder(|builder| {
            builder.parse_image_attributes(input, "image:", Default::default()).expect("failed to parse")
        });
        assert_eq!(attributes.width, expectation.map(Percent));
    }

    #[rstest]
    #[case::width("width:50%", Some(50))]
    #[case::empty("", None)]
    fn image_attributes_empty_prefix(#[case] input: &str, #[case] expectation: Option<u8>) {
        let attributes = Test::new("").with_builder(|builder| {
            builder.parse_image_attributes(input, "", Default::default()).expect("failed to parse")
        });
        assert_eq!(attributes.width, expectation.map(Percent));
    }
}