textiler-core 0.1.0

Provides the core utilities to get textiler to work
Documentation
use std::num::ParseIntError;
use std::ops::Deref;

use cssparser::{BasicParseError, BasicParseErrorKind, Parser, ParserInput, ToCss, Token};

use crate::theme::sx::sx_value::SxValue;
use crate::theme::Color;

type Result<T> = std::result::Result<T, ParseSxValueError>;

pub(super) fn parse_sx_value(input: &str) -> Result<SxValue> {
    let mut input = ParserInput::new(input);
    let mut parser = Parser::new(&mut input);

    let value = parse(&mut parser)?;
    Ok(match (value, parser.next().ok()) {
        (value, None) => value,
        (SxValue::CssLiteral(literal), Some(Token::Delim('.'))) => {
            let next = parser.next()?;
            let Token::Ident(ident) = next else {
                return Err(ParseSxValueError::UnexpectedToken(next.to_css_string()));
            };
            SxValue::ThemeToken {
                palette: literal,
                selector: ident.to_string(),
            }
        }
        (value, _) => value,
    })
}

fn parse(parser: &mut Parser) -> Result<SxValue> {
    let sx_value = match parser.next()? {
        Token::Ident(ident) => {
            if let Some((color, ..)) =
                cssparser::color::all_named_colors().find(|(color, ..)| *color == &**ident)
            {
                SxValue::Color(Color::CSSLiteral(color.to_string()))
            } else {
                SxValue::CssLiteral(ident.to_string())
            }
        }
        Token::Hash(hash) => SxValue::Color(Color::hex_code(u32::from_str_radix(&*hash, 16)?)),
        Token::QuotedString(quoted_str) => {
            let split = quoted_str.split(".").collect::<Vec<_>>();
            if split.len() == 2 {
                let palette = split[0].to_string();
                let selector = split[1].to_string();

                SxValue::ThemeToken { palette, selector }
            } else {
                SxValue::String(quoted_str.to_string())
            }
        }
        Token::Number {
            has_sign: _,
            value,
            int_value,
        } => {
            if let Some(int_value) = int_value {
                SxValue::Integer(*int_value)
            } else {
                SxValue::Float(*value)
            }
        }
        Token::Percentage {
            has_sign: _,
            unit_value,
            int_value,
        } => {
            if let &Some(int_value) = int_value {
                SxValue::Percent(int_value as f32 / 100.0)
            } else {
                SxValue::Percent(*unit_value)
            }
        }
        Token::Dimension {
            has_sign: _,
            value,
            int_value,
            unit,
        } => match int_value {
            None => SxValue::FloatDimension {
                value: *value,
                unit: unit.to_string(),
            },
            Some(value) => SxValue::Dimension {
                value: *value,
                unit: unit.to_string(),
            },
        },
        _tok => {
            return Err(ParseSxValueError::UnexpectedToken(_tok.to_css_string()));
        }
    };

    Ok(sx_value)
}

/// An error occurred while trying to parse this value
#[derive(Debug, thiserror::Error)]
pub enum ParseSxValueError {
    #[error("Unexpected token in input: {0:?}")]
    UnexpectedToken(String),
    #[error("An error occurred while trying to parse css")]
    CssParseError,
    #[error(transparent)]
    ParseIntError(#[from] ParseIntError),
}

impl<'a> From<BasicParseError<'a>> for ParseSxValueError {
    fn from(value: BasicParseError<'a>) -> Self {
        match &value.kind {
            BasicParseErrorKind::UnexpectedToken(tok) => {
                ParseSxValueError::UnexpectedToken(tok.to_css_string())
            }
            _err => ParseSxValueError::CssParseError,
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::theme::sx::sx_value::SxValue;
    use crate::theme::sx::sx_value_parsing::parse_sx_value;
    use crate::theme::Color;

    #[test]
    fn parse_theme_token() {
        let test = r#" "background.body" "#;

        let value = parse_sx_value(test).expect("could not parse");
        let SxValue::ThemeToken { palette, selector } = &value else {
            panic!("wrong sx value kind: {value:#?}");
        };

        assert_eq!(palette, "background");
        assert_eq!(selector, "body");
    }

    #[test]
    fn parse_colors() {
        let hex = "#0f0f0f";

        let value = parse_sx_value(hex).expect("could not parse");
        let SxValue::Color(color) = &value else {
            panic!("wrong sx value kind: {value:#?}");
        };
        assert!(matches!(color, crate::theme::Color::Hex(0x0f0f0f)));

        let red = "red";

        let value = parse_sx_value(hex).expect("could not parse");
        let SxValue::Color(color) = &value else {
            panic!("wrong sx value kind: {value:#?}");
        };
        let SxValue::Color(Color::CSSLiteral(color)) = value else {
            panic!("should be a color")
        };
        assert_eq!(color, "red");
    }

    #[test]
    fn parse_percent() {
        let percent = "15%";
        let SxValue::Percent(percent) = parse_sx_value(percent).expect("parse error") else {
            panic!("should parse percent");
        };

        assert_eq!(percent, 0.15);

        let percent = "15.3%";
        let SxValue::Percent(percent) = parse_sx_value(percent).expect("parse error") else {
            panic!("should parse percent");
        };

        assert_eq!(percent, 0.153);

        let percent = "-15.3%";
        let SxValue::Percent(percent) = parse_sx_value(percent).expect("parse error") else {
            panic!("should parse percent");
        };

        assert_eq!(percent, -0.153);
    }

    #[test]
    fn parse_dimension() {
        let width = "5px";

        let SxValue::Dimension { value, unit } = parse_sx_value(width).expect("parse error") else {
            panic!("should parse dimension");
        };

        assert_eq!(value, 5);
        assert_eq!(unit, "px");
    }
}