leaf-markdown-viewer 1.26.2

Terminal Markdown previewer with a GUI-like experience
use ratatui::style::Color;
use serde::{
    de::{Error as DeError, SeqAccess, Visitor},
    Deserialize, Deserializer,
};
use std::fmt;

use super::{resolution::parse_theme_color, MarkdownTheme, UiTheme};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct ThemeColor(pub(crate) Color);

impl<'de> Deserialize<'de> for ThemeColor {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct ColorVisitor;

        impl<'de> Visitor<'de> for ColorVisitor {
            type Value = ThemeColor;

            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("a color name, hex color string, rgb(...) string, or [r, g, b]")
            }

            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
            where
                E: DeError,
            {
                parse_theme_color(value)
                    .map(ThemeColor)
                    .ok_or_else(|| E::custom(format!("invalid theme color: {value}")))
            }

            fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
            where
                E: DeError,
            {
                self.visit_str(&value)
            }

            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
            where
                A: SeqAccess<'de>,
            {
                let r = read_rgb_component(&mut seq, "red")?;
                let g = read_rgb_component(&mut seq, "green")?;
                let b = read_rgb_component(&mut seq, "blue")?;
                if seq.next_element::<u16>()?.is_some() {
                    return Err(A::Error::custom(
                        "theme RGB array must contain exactly 3 values",
                    ));
                }
                Ok(ThemeColor(Color::Rgb(r, g, b)))
            }
        }

        deserializer.deserialize_any(ColorVisitor)
    }
}

pub(super) fn read_rgb_component<'de, A>(seq: &mut A, name: &str) -> Result<u8, A::Error>
where
    A: SeqAccess<'de>,
{
    let value = seq
        .next_element::<u16>()?
        .ok_or_else(|| A::Error::custom(format!("missing {name} theme color component")))?;
    u8::try_from(value)
        .map_err(|_| A::Error::custom(format!("{name} theme color component out of range")))
}

macro_rules! theme_overrides {
    ($name:ident for $theme:ty { $($field:ident),+ $(,)? }) => {
        #[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq)]
        #[serde(default)]
        pub(crate) struct $name {
            $(pub(crate) $field: Option<ThemeColor>,)+
        }

        impl $name {
            pub(super) fn apply_to(&self, theme: &mut $theme) {
                $(
                    if let Some(color) = self.$field {
                        theme.$field = color.0;
                    }
                )+
            }
        }
    };
}

theme_overrides!(UiThemeOverrides for UiTheme {
    toc_bg,
    toc_border,
    content_bg,
    scrollbar_hover,
    status_bg,
    status_separator,
    status_brand_fg,
    status_brand_bg,
    status_filename_fg,
    status_filename_bg,
    status_watch_fg,
    status_watch_bg,
    status_reloaded_fg,
    status_reloaded_bg,
    status_search_fg,
    status_search_bg,
    status_success_fg,
    status_success_bg,
    status_warning_fg,
    status_error_fg,
    status_error_bg,
    status_shortcut_fg,
    status_percent_fg,
    toc_hover_fg,
    toc_header_fg,
    toc_active_bg,
    toc_inactive_bg,
    toc_accent,
    toc_index_inactive,
    toc_primary_active,
    toc_primary_inactive,
    toc_secondary_inactive,
    toc_secondary_text_active,
    toc_secondary_text_inactive,
});

theme_overrides!(MarkdownThemeOverrides for MarkdownTheme {
    search_highlight_bg,
    search_match_bg,
    code_gutter,
    blockquote_marker,
    list_level_1,
    list_level_2,
    list_level_3,
    ordered_list,
    table_border,
    table_separator,
    table_header,
    table_cell,
    heading_1,
    heading_2,
    heading_3,
    heading_4,
    heading_other,
    heading_underline,
    code_frame,
    code_label,
    inline_code_fg,
    inline_code_bg,
    rule,
    link_icon,
    link_text,
    link_hover,
    blockquote_text,
    text,
    strong_text,
    latex_inline_fg,
    latex_inline_bg,
    latex_block_fg,
    mermaid_keyword,
    mermaid_arrow,
    mermaid_label,
    mermaid_block_fg,
    mark_fg,
    mark_bg,
    task_checked,
    task_unchecked,
    alert_note,
    alert_tip,
    alert_important,
    alert_warning,
    alert_caution,
});

#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq)]
#[serde(default)]
pub(crate) struct CustomThemeConfig {
    pub(crate) base: Option<String>,
    pub(crate) syntax: Option<String>,
    pub(crate) ui: UiThemeOverrides,
    pub(crate) markdown: MarkdownThemeOverrides,
}