tinted-builder 0.14.0

A Tinted Theming template builder which uses yaml color schemes to generate theme files.
Documentation
use crate::{utils::slugify, SchemeSystem, SchemeVariant};
use serde::ser::{SerializeMap, SerializeStruct};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::{collections::HashMap, fmt};

pub use crate::scheme::color::Color;

pub const REQUIRED_BASE16_PALETTE_KEYS: [&str; 16] = [
    "base00", "base01", "base02", "base03", "base04", "base05", "base06", "base07", "base08",
    "base09", "base0A", "base0B", "base0C", "base0D", "base0E", "base0F",
];

#[derive(Deserialize, Serialize)]
struct SchemeWrapper {
    pub(crate) system: SchemeSystem,
    pub(crate) name: String,
    pub(crate) slug: Option<String>,
    pub(crate) author: String,
    pub(crate) description: Option<String>,
    pub(crate) variant: Option<SchemeVariant>,
    pub(crate) palette: HashMap<String, String>,
}

/// Deserialized Base16 scheme with normalized palette and metadata.
///
/// The `palette` map contains Base16 color keys (`base00` through `base0F`) mapped to
/// normalized `Color` values. Serialization preserves sorted palette keys and hex values
/// with a leading `#`.
#[derive(Debug, Clone)]
pub struct Scheme {
    pub system: SchemeSystem,
    pub name: String,
    pub slug: String,
    pub author: String,
    pub description: Option<String>,
    pub variant: SchemeVariant,
    pub palette: HashMap<String, Color>,
}

impl fmt::Display for Scheme {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f, "author: \"{}\"", self.author)?;
        if let Some(ref desc) = self.description {
            writeln!(f, "description: \"{desc}\"")?;
        }
        writeln!(f, "name: \"{}\"", self.name)?;
        writeln!(f, "slug: \"{}\"", self.slug)?;
        writeln!(f, "system: \"{}\"", self.system)?;
        writeln!(f, "variant: \"{}\"", self.variant)?;
        writeln!(f, "palette:")?;

        let mut palette_vec: Vec<(String, Color)> = self
            .palette
            .clone()
            .iter()
            .map(|(k, v)| (k.clone(), v.clone()))
            .collect();
        palette_vec.sort_by_key(|k| k.0.clone());

        for (key, value) in palette_vec {
            writeln!(f, "  {key}: \"{value}\"")?;
        }
        Ok(())
    }
}

impl<'de> Deserialize<'de> for Scheme {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let wrapper = SchemeWrapper::deserialize(deserializer)?;
        let slug = wrapper
            .slug
            .map_or_else(|| slugify(&wrapper.name), |slug| slugify(&slug));
        let variant = wrapper.variant.unwrap_or(SchemeVariant::Dark);

        if wrapper.system != SchemeSystem::Base16 {
            return Err(serde::de::Error::custom(format!(
                "{} is not a valid system for a Base16 scheme",
                wrapper.system
            )));
        }

        let contains_all_keys = REQUIRED_BASE16_PALETTE_KEYS
            .iter()
            .all(|&key| wrapper.palette.contains_key(key));

        if !contains_all_keys {
            return Err(serde::de::Error::custom(
                "base16 scheme does not contain the required palette properties",
            ));
        }

        let palette_result: Result<HashMap<String, Color>, _> = wrapper
            .palette
            .into_iter()
            .map(|(key, value)| {
                Color::new(&value, None, None)
                    .map(|color| (key, color))
                    .map_err(|e| serde::de::Error::custom(e.to_string()))
            })
            .collect();

        Ok(Self {
            name: wrapper.name,
            slug,
            system: wrapper.system,
            author: wrapper.author,
            description: wrapper.description,
            variant,
            palette: palette_result?,
        })
    }
}

impl Serialize for Scheme {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut state = serializer.serialize_struct("Scheme", 7)?;
        state.serialize_field("system", &self.system)?;
        state.serialize_field("name", &self.name)?;
        state.serialize_field("slug", &self.slug)?;
        state.serialize_field("author", &self.author)?;
        if let Some(description) = &self.description {
            state.serialize_field("description", description)?;
        }
        state.serialize_field("variant", &self.variant)?;

        // Collect and sort the palette by key
        let mut sorted_palette: Vec<(&String, &Color)> = self.palette.iter().collect();
        sorted_palette.sort_by(|a, b| a.0.cmp(b.0));

        // Serialize the sorted palette as a map within the struct
        state.serialize_field("palette", &SortedPalette(sorted_palette))?;

        state.end()
    }
}

// Helper struct for serializing sorted palette
struct SortedPalette<'a>(Vec<(&'a String, &'a Color)>);

#[allow(clippy::elidable_lifetime_names)]
impl<'a> Serialize for SortedPalette<'a> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut map = serializer.serialize_map(Some(self.0.len()))?;
        for (key, value) in &self.0 {
            map.serialize_entry(key, format!("#{}", &value.to_hex()).as_str())?;
        }
        map.end()
    }
}