logo-parser 1.0.2

A library for parsing custom logo and color configurations from JSON5 files for LynxFetch
Documentation
// SPDX-FileCopyrightText: 2023 CELESTIFYX Team
// SPDX-License-Identifier: GPL-3.0-or-later

use std::{
    fs,
    path::Path,
    str::FromStr,
    num::ParseIntError,

    io::{
        self,
        ErrorKind
    }
};

use chromakitx::{
    AnsiColor,
    CssColor,
    XtermColor,
    AnyColor
};

use super::{
    error::Error,

    config::{
        LogoConfig,
        ColorConfig,
        ColorDef
    }
};

pub struct LogoParser;

impl LogoParser {
    pub fn from(path: &Path) -> Result<(Vec<String>, Vec<String>, ColorConfig), Error> {
        let content: String = fs::read_to_string(path).map_err(|err: io::Error| {
            if err.kind() == ErrorKind::NotFound {
                return Error::FileNotFound(path.to_path_buf());
            }

            Error::IoError(err)
        })?;

        let config: LogoConfig = json5::from_str(&content)?;
        config.validate().map_err(Error::ValidationError)?;

        let large: Vec<String> = config.get_large_logo().unwrap_or_default();
        let small: Vec<String> = config.get_small_logo().unwrap_or_default();

        if large.is_empty() && small.is_empty() {
            return Err(Error::ValidationError("No valid logo found".to_string()));
        }

        let color_config: ColorConfig = Self::parse_colors(&config)?;
        Ok((large, small, color_config))
    }

    fn parse_colors(config: &LogoConfig) -> Result<ColorConfig, Error> {
        let color_defs: &[ColorDef] = config.get_color_defs().ok_or_else(|| Error::ValidationError("Color definitions not found".to_string()))?;

        if color_defs.is_empty() {
            return Err(Error::ValidationError("Color definitions array is empty".to_string()));
        }

        let title_ref: &str = config.get_title_color_ref().ok_or_else(|| Error::ValidationError("Title color reference not found".to_string()))?;
        let title_color: AnyColor = Self::resolve_color_reference(title_ref, color_defs)?;

        let keys_ref: &str = config.get_keys_color_ref().ok_or_else(|| Error::ValidationError("Keys color reference not found".to_string()))?;
        let keys_color: AnyColor = Self::resolve_color_reference(keys_ref, color_defs)?;

        let mut ascii_colors: Vec<AnyColor> = Vec::with_capacity(color_defs.len());

        for color_def in color_defs {
            (&mut ascii_colors).push(Self::parse_color_def(color_def)?);
        }

        Ok(ColorConfig {
            title_color,
            keys_color,
            ascii_colors
        })
    }

    fn resolve_color_reference(reference: &str, color_defs: &[ColorDef]) -> Result<AnyColor, Error> {
        let reference: &str = reference.trim();

        if let Some(idx_str) = reference.strip_prefix('%') {
            let idx: usize = idx_str.parse().map_err(|_: ParseIntError| Error::ColorParseError(format!("Invalid color index: {}", idx_str)))?;

            if idx >= color_defs.len() {
                return Err(Error::ColorParseError(format!("Color index out of bounds: {}", idx), ));
            }

            return Self::parse_color_def(&color_defs[idx]);
        }

        Self::parse_color_name(reference)
    }

    fn parse_color_def(color_def: &ColorDef) -> Result<AnyColor, Error> {
        match color_def {
            ColorDef::Simple(name) => Self::parse_color_name(name),

            ColorDef::Named(map) => {
                if map.len() != 1 {
                    return Err(Error::ColorParseError("Color definition object must have exactly one entry".to_string()));
                }

                let (color_name, color_type): (&String, &String) = map.iter().next().ok_or_else(|| Error::ColorParseError("Empty color definition".to_string()))?;

                if color_type.eq_ignore_ascii_case("ansi") {
                    Self::parse_ansi_color(color_name)
                } else if color_type.eq_ignore_ascii_case("xterm") {
                    Self::parse_xterm_color(color_name)
                } else if color_type.eq_ignore_ascii_case("css") {
                    Self::parse_css_color(color_name)
                } else {
                    Err(Error::ColorParseError(format!("Unknown color type: {}", color_type)))
                }
            }
        }
    }

    fn parse_color_name(name: &str) -> Result<AnyColor, Error> {
        AnsiColor::from_str(name).map(AnyColor::Ansi).or_else(|_: String| {
            CssColor::from_str(name).map(AnyColor::Css)
        }).or_else(|_: String| {
            XtermColor::from_str(name).map(AnyColor::Xterm)
        }).map_err(|_: String| Error::ColorParseError(format!("Unknown color name: {}", name)))
    }

    fn parse_ansi_color(name: &str) -> Result<AnyColor, Error> {
        AnsiColor::from_str(name).map(AnyColor::Ansi).map_err(|_: String| Error::ColorParseError(format!("Unknown ANSI color: {}", name)))
    }

    fn parse_xterm_color(name: &str) -> Result<AnyColor, Error> {
        XtermColor::from_str(name).map(AnyColor::Xterm).map_err(|_: String| Error::ColorParseError(format!("Unknown Xterm color: {}", name)))
    }

    fn parse_css_color(name: &str) -> Result<AnyColor, Error> {
        CssColor::from_str(name).map(AnyColor::Css).map_err(|_: String| Error::ColorParseError(format!("Unknown CSS color: {}", name)))
    }
}