alchemy-styles 0.1.0

Style parsing and hoisting for Alchemy, the Rust cross-platform GUI framework.
Documentation
//! CSS parsing logic. Mostly relies from the rust-cssparser crate,
//! slightly modified to fit the `Styles` structure we want internally.

use cssparser::{
    AtRuleParser, BasicParseError, CowRcStr,
    DeclarationListParser, DeclarationParser,
    Parser, ParseError, QualifiedRuleParser,
    SourceLocation, Token
};

use crate::styles::*;

/// Represents a style rule, a `key: [values...];` pair.
#[derive(Debug)]
pub struct Rule {
    pub key: String,
    pub styles: Vec<Styles>
}

/// The parser itself.
#[derive(Debug)]
pub struct RuleParser;

/// Some type information for our parser.
impl<'i> AtRuleParser<'i> for RuleParser {
    type PreludeBlock = ();
    type PreludeNoBlock = ();
    type AtRule = Rule;
    type Error = BasicParseError<'i>;
}

/// The actual work our parser does. Walks style rules and attempts to
/// extract the key/value pairings from a given stylesheet string.
impl<'i> QualifiedRuleParser<'i> for RuleParser {
    type Prelude = String;
    type QualifiedRule = Rule;
    type Error = BasicParseError<'i>;

    /// Parses out the selector.
    fn parse_prelude<'t>(
        &mut self,
        input: &mut Parser<'i, 't>,
    ) -> Result<Self::Prelude, ParseError<'i, Self::Error>> {
        let location = input.current_source_location();

        let selector = match input.next()? {
            Token::Ident(ref element_name) => element_name.to_string(),
            t => { return Err(location.new_unexpected_token_error(t.clone())); }
        };

        // If there's a next, someone is writing their code assuming cascading. Let's... warn them.
        /*match input.next()? {
            Ok(_) => {},
            Err(e) => {}
        };*/

        Ok(selector)
    }

    /// Parses the block (`{...}`) into a Rule struct.
    fn parse_block<'t>(
        &mut self,
        key: Self::Prelude,
        _location: SourceLocation,
        input: &mut Parser<'i, 't>,
    ) -> Result<Self::QualifiedRule, ParseError<'i, Self::Error>> {
        let styles = DeclarationListParser::new(input, StyleParser {}).collect::<Vec<_>>();

        Ok(Rule {
            key: key,
            styles: styles.into_iter().filter_map(|decl| {
                if !decl.is_ok() {
                    eprintln!("{:?}", decl);
                }

                decl.ok()
            }).collect()
        })
    }
}

/// Contains logic for matching CSS attributes to their `Styles` counterpart.
#[derive(Debug)]
pub struct StyleParser;

/// Types, etc.
impl<'i> AtRuleParser<'i> for StyleParser {
    type PreludeBlock = ();
    type PreludeNoBlock = ();
    type AtRule = Styles;
    type Error = BasicParseError<'i>;
}

/// A utility method for dereferencing a value, to make some code later on a bit more clean.
fn ident<'a>(token: &'a Token) -> &'a str {
    match token {
        Token::Ident(ref value) => &*value,
        _ => ""
    }
}

impl<'i> DeclarationParser<'i> for StyleParser {
    type Declaration = Styles;
    type Error = BasicParseError<'i>;

    /// Parses a value (e.g, `background-color: #307ace;`) into a `Styles` value.
    fn parse_value<'t>(
        &mut self,
        name: CowRcStr<'i>,
        input: &mut Parser<'i, 't>,
    ) -> Result<Self::Declaration, ParseError<'i, Self::Error>> {
        let style = match &*name {
            "align-content" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
                "flex-start" => Styles::AlignContent(AlignContent::FlexStart),
                "flex-end" => Styles::AlignContent(AlignContent::FlexEnd),
                "center" => Styles::AlignContent(AlignContent::Center),
                "stretch" => Styles::AlignContent(AlignContent::Stretch),
                "space-between" => Styles::AlignContent(AlignContent::SpaceBetween),
                "space-around" => Styles::AlignContent(AlignContent::SpaceAround),
                _ => { return Err(s.new_unexpected_token_error(t.clone())); }
            }},
            
            "align-items" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
                "flex-start" => Styles::AlignItems(AlignItems::FlexStart),
                "flex-end" => Styles::AlignItems(AlignItems::FlexEnd),
                "center" => Styles::AlignItems(AlignItems::Center),
                "baseline" => Styles::AlignItems(AlignItems::Baseline),
                "stretch" => Styles::AlignItems(AlignItems::Stretch),
                _ => { return Err(s.new_unexpected_token_error(t.clone())); }
            }},
            
            "align_self" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
                "auto" => Styles::AlignSelf(AlignSelf::Auto),
                "flex-start" => Styles::AlignSelf(AlignSelf::FlexStart),
                "flex-end" => Styles::AlignSelf(AlignSelf::FlexEnd),
                "center" => Styles::AlignSelf(AlignSelf::Center),
                "baseline" => Styles::AlignSelf(AlignSelf::Baseline),
                "stretch" => Styles::AlignSelf(AlignSelf::Stretch),
                _ => { return Err(s.new_unexpected_token_error(t.clone())); }
            }},

            // @TODO: Aspect Ratio... could be string, no? Should this be handled better?
            "aspect-ratio" => Styles::AspectRatio(Number::Defined(parse_floaty_mcfloatface_value(input)?)),

            "backface-visibility" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
                "visible" => Styles::BackfaceVisibility(BackfaceVisibility::Visible),
                "hidden" => Styles::BackfaceVisibility(BackfaceVisibility::Hidden),
                _ => { return Err(s.new_unexpected_token_error(t.clone())); }
            }},

            "background-color" => Styles::BackgroundColor(Color::parse(input)?),
            
            // Border values~
            "border-color" => Styles::BorderColor(Color::parse(input)?),
            "border-top-color" => Styles::BorderTopColor(Color::parse(input)?),
            "border-bottom-color" => Styles::BorderBottomColor(Color::parse(input)?),
            "border-left-color" => Styles::BorderLeftColor(Color::parse(input)?),
            "border-right-color" => Styles::BorderRightColor(Color::parse(input)?),
            
            "bottom" => Styles::Bottom(parse_floaty_mcfloatface_value(input)?),

            "color" => Styles::TextColor(Color::parse(input)?),

            "direction" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
                "inherit" => Styles::Direction(Direction::Inherit),
                "ltr" => Styles::Direction(Direction::LTR),
                "rtl" => Styles::Direction(Direction::RTL),
                _ => { return Err(s.new_unexpected_token_error(t.clone())); }
            }},

            "display" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
                "flex" => Styles::Display(Display::Flex),
                "none" => Styles::Display(Display::None),
                _ => { return Err(s.new_unexpected_token_error(t.clone())); }
            }},
            
            "end" => Styles::End(parse_floaty_mcfloatface_value(input)?),

            "flex-basis" => Styles::FlexBasis(parse_floaty_mcfloatface_value(input)?),
            
            "flex-direction" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
                "row" => Styles::FlexDirection(FlexDirection::Row),
                "row-reverse" => Styles::FlexDirection(FlexDirection::RowReverse),
                "column" => Styles::FlexDirection(FlexDirection::Column),
                "column-reverse" => Styles::FlexDirection(FlexDirection::ColumnReverse),
                _ => { return Err(s.new_unexpected_token_error(t.clone())); }
            }},

            "flex-grow" => Styles::FlexGrow(parse_floaty_mcfloatface_value(input)?),
            "flex-shrink" => Styles::FlexShrink(parse_floaty_mcfloatface_value(input)?),
            
            "flex-wrap" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
                "no-wrap" => Styles::FlexWrap(FlexWrap::NoWrap),
                "wrap" => Styles::FlexWrap(FlexWrap::Wrap),
                "wrap-reverse" => Styles::FlexWrap(FlexWrap::WrapReverse),
                _ => { return Err(s.new_unexpected_token_error(t.clone())); }
            }},
            
            //FontFamily(FontFamily),
            "font-size" => Styles::FontSize(parse_floaty_mcfloatface_value(input)?),
            
            "font-style" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
                "normal" => Styles::FontStyle(FontStyle::Normal),
                "italic" => Styles::FontStyle(FontStyle::Italic),
                "oblique" => Styles::FontStyle(FontStyle::Oblique),
                _ => { return Err(s.new_unexpected_token_error(t.clone())); }
            }},

            "font-weight" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
                "normal" => Styles::FontWeight(FontWeight::Normal),
                "bold" => Styles::FontWeight(FontWeight::Bold),
                _ => { return Err(s.new_unexpected_token_error(t.clone())); }
            }},
            
            "height" => Styles::Height(parse_floaty_mcfloatface_value(input)?),

            "justify-content" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
                "flex-start" => Styles::JustifyContent(JustifyContent::FlexStart),
                "flex-end" => Styles::JustifyContent(JustifyContent::FlexEnd),
                "center" => Styles::JustifyContent(JustifyContent::Center),
                "space-between" => Styles::JustifyContent(JustifyContent::SpaceBetween),
                "space-around" => Styles::JustifyContent(JustifyContent::SpaceAround),
                "space-evenly" => Styles::JustifyContent(JustifyContent::SpaceEvenly),
                _ => { return Err(s.new_unexpected_token_error(t.clone())); }
            }},
            
            "left" => Styles::Left(parse_floaty_mcfloatface_value(input)?),
            "line-height" => Styles::FontLineHeight(parse_floaty_mcfloatface_value(input)?),

            "margin-bottom" => Styles::MarginBottom(parse_floaty_mcfloatface_value(input)?),
            "margin-end" => Styles::MarginEnd(parse_floaty_mcfloatface_value(input)?),
            "margin-left" => Styles::MarginLeft(parse_floaty_mcfloatface_value(input)?),
            "margin-right" => Styles::MarginRight(parse_floaty_mcfloatface_value(input)?),
            "margin-start" => Styles::MarginStart(parse_floaty_mcfloatface_value(input)?),
            "margin-top" => Styles::MarginTop(parse_floaty_mcfloatface_value(input)?),

            "max-height" => Styles::MaxHeight(parse_floaty_mcfloatface_value(input)?),
            "max-width" => Styles::MaxWidth(parse_floaty_mcfloatface_value(input)?),
            
            "min-height" => Styles::MinHeight(parse_floaty_mcfloatface_value(input)?),
            "min-width" => Styles::MinWidth(parse_floaty_mcfloatface_value(input)?),

            "opacity" => Styles::Opacity(parse_floaty_mcfloatface_value(input)?),
            
            "overflow" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
                "visible" => Styles::Overflow(Overflow::Visible),
                "hidden" => Styles::Overflow(Overflow::Hidden),
                "scroll" => Styles::Overflow(Overflow::Scroll),
                _ => { return Err(s.new_unexpected_token_error(t.clone())); }
            }},
            
            "padding-bottom" => Styles::PaddingBottom(parse_floaty_mcfloatface_value(input)?),
            "padding-end" => Styles::PaddingEnd(parse_floaty_mcfloatface_value(input)?),
            "padding-left" => Styles::PaddingLeft(parse_floaty_mcfloatface_value(input)?),
            "padding-right" => Styles::PaddingRight(parse_floaty_mcfloatface_value(input)?),
            "padding-start" => Styles::PaddingStart(parse_floaty_mcfloatface_value(input)?),
            "padding-top" => Styles::PaddingTop(parse_floaty_mcfloatface_value(input)?),
            
            "position" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
                "absolute" => Styles::PositionType(PositionType::Absolute),
                "relative" => Styles::PositionType(PositionType::Relative),
                _ => { return Err(s.new_unexpected_token_error(t.clone())); }
            }},
            
            "right" => Styles::Right(parse_floaty_mcfloatface_value(input)?),
            "start" => Styles::Start(parse_floaty_mcfloatface_value(input)?),
            
            "text-align" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
                "auto" => Styles::TextAlignment(TextAlignment::Auto),
                "left" => Styles::TextAlignment(TextAlignment::Left),
                "right" => Styles::TextAlignment(TextAlignment::Right),
                "center" => Styles::TextAlignment(TextAlignment::Center),
                "justify" => Styles::TextAlignment(TextAlignment::Justify),
                _ => { return Err(s.new_unexpected_token_error(t.clone())); }
            }},
            
            "text-decoration-color" => Styles::TextDecorationColor(Color::parse(input)?),
            "text-shadow-color" => Styles::TextShadowColor(Color::parse(input)?),
            "tint-color" => Styles::TintColor(Color::parse(input)?),
            
            "top" => Styles::Top(parse_floaty_mcfloatface_value(input)?),
            "width" => Styles::Width(parse_floaty_mcfloatface_value(input)?),
            
            t => {
                let location = input.current_source_location();
                return Err(location.new_unexpected_token_error(Token::Ident(t.to_string().into())));
            }
        };

        Ok(style)
    }
}

/// A utility method for handling some float values.
/// Mostly used to reduce code verbosity in the massive switch table for `Styles` parsing.
fn parse_floaty_mcfloatface_value<'i, 't>(input: &mut Parser<'i, 't>) -> Result<f32, BasicParseError<'i>> {
    let location = input.current_source_location();
    let token = input.next()?;

    match token {
        Token::Number { value, .. } => Ok(*value),    
        _ => Err(location.new_basic_unexpected_token_error(token.clone()))
    }
}