tinted-builder 0.14.0

A Tinted Theming template builder which uses yaml color schemes to generate theme files.
Documentation
pub mod meta;
pub mod palette;
pub mod syntax;
pub mod ui;

pub use crate::scheme::tinted8::structure::meta::SchemeMeta;
pub use crate::scheme::tinted8::structure::palette::Palette;
pub use crate::scheme::tinted8::structure::syntax::{Syntax, SyntaxKey};
pub use crate::scheme::tinted8::structure::ui::Ui;
use crate::scheme::tinted8::yaml::Tinted8Scheme as YamlTinted8Scheme;
use crate::tinted8::SUPPORTED_STYLING_SPEC_VERSION;
use crate::utils::slugify;
use crate::utils::titlecasify;
use crate::{SchemeSupports, SchemeVariant};
use semver::{Version, VersionReq};
use serde::{Deserialize, Deserializer, Serialize};
use std::fmt;

/// Fully resolved Tinted8 scheme used by templates and downstream tooling.
///
/// This structure is created from YAML via `tinted8::yaml`, deriving missing variants and
/// supplemental colors according to the Tinted8 builder rules.
#[derive(Debug, Clone, Serialize)]
pub struct Scheme {
    pub scheme: SchemeMeta,
    pub palette: Palette,
    pub syntax: Syntax,
    pub ui: Ui,
    pub variant: SchemeVariant,
}

impl fmt::Display for Scheme {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f, "scheme:")?;
        writeln!(f, "  system: \"{}\"", self.scheme.system)?;
        writeln!(
            f,
            "    supports: \n        \"{}\"",
            self.scheme.supports.styling_spec
        )?;
        writeln!(f, "  name: \"{}\"", self.scheme.name)?;
        writeln!(f, "  author: \"{}\"", self.scheme.author)?;
        writeln!(f, "  theme-author: \"{}\"", self.scheme.theme_author)?;
        writeln!(f, "  slug: \"{}\"", self.scheme.slug)?;
        if let Some(ref desc) = self.scheme.description {
            writeln!(f, "  description: \"{desc}\"")?;
        }
        #[allow(clippy::writeln_empty_string)]
        writeln!(f, "")?;
        writeln!(f, "variant: \"{}\"", self.variant)?;
        if let Some(ref family) = self.scheme.family {
            writeln!(f, "family: \"{family}\"")?;
        }
        if let Some(ref style) = self.scheme.style {
            writeln!(f, "style: \"{style}\"")?;
        }

        #[allow(clippy::writeln_empty_string)]
        writeln!(f, "")?;

        writeln!(f, "palette:")?;

        #[allow(clippy::writeln_empty_string)]
        writeln!(f, "")?;

        writeln!(f, "{}", self.palette)?;

        writeln!(f, "syntax:")?;
        writeln!(f, "{}", self.syntax)?;

        writeln!(f, "ui:")?;
        writeln!(f, "{}", self.ui)?;

        Ok(())
    }
}

impl<'de> Deserialize<'de> for Scheme {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let wrapper = YamlTinted8Scheme::deserialize(deserializer)?;

        let (name, slug): (String, String) = match (
            &wrapper.scheme.name,
            &wrapper.scheme.slug,
            &wrapper.scheme.family,
            &wrapper.scheme.style,
        ) {
            (Some(name), Some(slug), _, _) => (name.to_owned(), slug.to_owned()),
            (Some(name), None, _, _) => (name.to_owned(), slugify(name)),
            (None, Some(slug), _, _) => (titlecasify(slug), slug.to_owned()),
            (None, None, Some(family), Some(style)) => {
                let display_family = titlecasify(family);
                let display_style = titlecasify(style);
                let name = format!("{display_family} {display_style}");

                (name.clone(), slugify(&name))
            }
            (None, None, Some(family), None) => {
                let name = titlecasify(&family.clone());

                (name, slugify(&family.clone()))
            }
            _ => {
                return Err(serde::de::Error::custom(
                    "Either 'name', 'slug' or 'family' must exist in yaml scheme",
                ))
            }
        };

        let palette =
            Palette::try_from_basic(&wrapper.palette).map_err(serde::de::Error::custom)?;
        let ui = Ui::try_from_basic(&wrapper.ui.unwrap_or_default(), &palette, &wrapper.variant)
            .map_err(serde::de::Error::custom)?;
        let syntax = Syntax::try_from_basic(
            &wrapper.syntax.unwrap_or_default(),
            &palette,
            &wrapper.variant,
        )
        .map_err(serde::de::Error::custom)?;

        let styling_spec = VersionReq::parse(&wrapper.scheme.supports.styling_spec)
            .map_err(serde::de::Error::custom)?;
        let styling_req =
            Version::parse(SUPPORTED_STYLING_SPEC_VERSION).map_err(serde::de::Error::custom)?;
        if !styling_spec.matches(&styling_req) {
            return Err(serde::de::Error::custom(
                format!("E002: Unsupported Tinted8 Styling Spec (requires {styling_req}, supported v{styling_spec})")
            ));
        }
        let styling_spec = styling_spec
            .to_string()
            .strip_prefix("^")
            .unwrap_or(&styling_spec.to_string())
            .to_string();

        let scheme_meta = SchemeMeta {
            name,
            slug,
            system: wrapper.scheme.system,
            author: wrapper.scheme.author.clone(),
            description: wrapper.scheme.description,
            theme_author: wrapper.scheme.theme_author.unwrap_or(wrapper.scheme.author),
            supports: SchemeSupports { styling_spec },
            family: wrapper.scheme.family,
            style: wrapper.scheme.style,
        };

        Ok(Self {
            scheme: scheme_meta,
            variant: wrapper.variant,
            syntax,
            ui,
            palette,
        })
    }
}