Kaynağa Gözat

Chore: add in style crate, and abort any styligng

Jonathan Kelley 4 yıl önce
ebeveyn
işleme
c09b71f

+ 0 - 1
packages/core-macro/Cargo.toml

@@ -16,7 +16,6 @@ proc-macro-hack = "0.5.19"
 proc-macro2 = "1.0.6"
 quote = "1.0"
 syn = { version = "1.0.11", features = ["full"] }
-style-shared = { git = "https://github.com/derekdreery/style" }
 
 # testing
 [dev-dependencies]

+ 11 - 10
packages/core-macro/src/htm.rs

@@ -17,7 +17,6 @@ use {
     proc_macro::TokenStream,
     proc_macro2::{Span, TokenStream as TokenStream2},
     quote::{quote, ToTokens, TokenStreamExt},
-    style_shared::Styles,
     syn::{
         ext::IdentExt,
         parse::{Parse, ParseStream},
@@ -267,15 +266,17 @@ impl Parse for Attr {
                 let outer;
                 syn::braced!(outer in s);
                 // double brace for inline style.
-                if outer.peek(token::Brace) {
-                    let inner;
-                    syn::braced!(inner in outer);
-                    let styles: Styles = inner.parse()?;
-                    MaybeExpr::Literal(LitStr::new(&styles.to_string(), Span::call_site()))
-                } else {
-                    // just parse as an expression
-                    MaybeExpr::Expr(outer.parse()?)
-                }
+                // todo!("Style support not ready yet");
+
+                // if outer.peek(token::Brace) {
+                //     let inner;
+                //     syn::braced!(inner in outer);
+                //     let styles: Styles = inner.parse()?;
+                //     MaybeExpr::Literal(LitStr::new(&styles.to_string(), Span::call_site()))
+                // } else {
+                // just parse as an expression
+                MaybeExpr::Expr(outer.parse()?)
+            // }
             } else {
                 s.parse()?
             };

+ 1 - 0
packages/core-macro/src/lib.rs

@@ -12,6 +12,7 @@ use syn::{
 mod fc;
 mod htm;
 mod ifmt;
+// mod styles;
 
 /// The html! macro makes it easy for developers to write jsx-style markup in their components.
 /// We aim to keep functional parity with html templates.

+ 264 - 0
packages/core-macro/src/styles/calc.rs

@@ -0,0 +1,264 @@
+//! The `calc` functionality.
+use crate::LengthPercentage;
+use ::{
+    proc_macro2::TokenStream,
+    quote::{quote, ToTokens},
+    std::fmt,
+    syn::{
+        custom_keyword, parenthesized,
+        parse::{Parse, ParseStream},
+        Token,
+    },
+};
+
+/// Values that can be a calculaion (currently restricted to length & percentages)
+#[derive(Debug, Clone, PartialEq)]
+pub enum Calc {
+    Calculated(CalcSum),
+    Normal(LengthPercentage),
+}
+
+impl fmt::Display for Calc {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Calc::Calculated(inner) => write!(f, "calc({})", inner),
+            Calc::Normal(inner) => write!(f, "{}", inner),
+        }
+    }
+}
+
+impl Parse for Calc {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        custom_keyword!(calc);
+        if s.peek(calc) {
+            s.parse::<calc>()?;
+            let content;
+            parenthesized!(content in s);
+            Ok(Calc::Calculated(content.parse()?))
+        } else {
+            Ok(Calc::Normal(s.parse()?))
+        }
+    }
+}
+
+impl ToTokens for Calc {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            Calc::Calculated(inner) => quote!(style::Calc::Calculated(#inner)),
+            Calc::Normal(inner) => quote!(style::Calc::Normal(#inner)),
+        });
+    }
+}
+
+#[test]
+fn test_calc() {
+    for (input, output) in vec![
+        ("calc(10% - 20\"em\")", "calc(10% - 20em)"),
+        ("calc(100% + 5px)", "calc(100% + 5px)"),
+        ("calc(100% - 60px)", "calc(100% - 60px)"),
+    ] {
+        assert_eq!(&syn::parse_str::<Calc>(input).unwrap().to_string(), output);
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct CalcSum {
+    pub first: CalcProduct,
+    pub rest: Vec<SumOp>,
+}
+
+impl fmt::Display for CalcSum {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{}", self.first)?;
+        for op in self.rest.iter() {
+            write!(f, "{}", op)?;
+        }
+        Ok(())
+    }
+}
+
+impl Parse for CalcSum {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let first: CalcProduct = s.parse()?;
+        let mut rest: Vec<SumOp> = vec![];
+        while SumOp::peek(s) {
+            rest.push(s.parse()?);
+        }
+        Ok(CalcSum { first, rest })
+    }
+}
+
+impl ToTokens for CalcSum {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        let first = &self.first;
+        let rest = self.rest.iter();
+        tokens.extend(quote! {
+            style::calc::CalcSum {
+                first: #first,
+                rest: vec![#(#rest,)*]
+            }
+        });
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum SumOp {
+    Add(CalcProduct),
+    Sub(CalcProduct),
+}
+
+impl SumOp {
+    fn peek(s: ParseStream) -> bool {
+        s.peek(Token![+]) || s.peek(Token![-])
+    }
+}
+
+impl fmt::Display for SumOp {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            SumOp::Add(inner) => write!(f, " + {}", inner),
+            SumOp::Sub(inner) => write!(f, " - {}", inner),
+        }
+    }
+}
+
+impl Parse for SumOp {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let lookahead = s.lookahead1();
+        if lookahead.peek(Token![+]) {
+            s.parse::<Token![+]>()?;
+            Ok(SumOp::Add(s.parse()?))
+        } else if lookahead.peek(Token![-]) {
+            s.parse::<Token![-]>()?;
+            Ok(SumOp::Sub(s.parse()?))
+        } else {
+            Err(lookahead.error())
+        }
+    }
+}
+
+impl ToTokens for SumOp {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            SumOp::Add(inner) => quote!(style::SumOp::Add(#inner)),
+            SumOp::Sub(inner) => quote!(style::SumOp::Sub(#inner)),
+        });
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct CalcProduct {
+    pub first: CalcValue,
+    pub rest: Vec<ProductOp>,
+}
+
+impl fmt::Display for CalcProduct {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{}", self.first)?;
+        for op in self.rest.iter() {
+            write!(f, "{}", op)?;
+        }
+        Ok(())
+    }
+}
+
+impl Parse for CalcProduct {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let first: CalcValue = s.parse()?;
+        let mut rest: Vec<ProductOp> = vec![];
+        while ProductOp::peek(s) {
+            rest.push(s.parse()?);
+        }
+        Ok(CalcProduct { first, rest })
+    }
+}
+
+impl ToTokens for CalcProduct {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        let first = &self.first;
+        let rest = self.rest.iter();
+        tokens.extend(quote! {
+            style::calc::CalcProduct {
+                first: #first,
+                rest: vec![#(#rest,)*]
+            }
+        });
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum ProductOp {
+    Mul(CalcValue),
+    // todo Div(Number),
+}
+
+impl ProductOp {
+    pub fn peek(s: ParseStream) -> bool {
+        s.peek(Token![*]) // || s.peek(Token[/])
+    }
+}
+
+impl fmt::Display for ProductOp {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            ProductOp::Mul(inner) => write!(f, "*{}", inner),
+            //ProductOp::Div(inner) => write!(f, "/{}", inner),
+        }
+    }
+}
+
+impl Parse for ProductOp {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let lookahead = s.lookahead1();
+        if lookahead.peek(Token![*]) {
+            s.parse::<Token![*]>()?;
+            Ok(ProductOp::Mul(s.parse()?))
+        /*
+        } else if lookahead.peek(Token![/]) {
+            s.parse::<Token![/]>()?;
+            Ok(ProductOp::Div(s.parse()?))
+        */
+        } else {
+            Err(lookahead.error())
+        }
+    }
+}
+
+impl ToTokens for ProductOp {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            ProductOp::Mul(inner) => quote!(style::ProductOp::Mul(#inner)),
+            //ProductOp::Div(inner) => quote!(style::ProductOp::Div(#inner)),
+        });
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum CalcValue {
+    LengthPercentage(LengthPercentage),
+    // todo more variants
+}
+
+impl Parse for CalcValue {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        Ok(CalcValue::LengthPercentage(s.parse()?))
+    }
+}
+
+impl fmt::Display for CalcValue {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            CalcValue::LengthPercentage(inner) => write!(f, "{}", inner),
+        }
+    }
+}
+
+impl ToTokens for CalcValue {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            CalcValue::LengthPercentage(inner) => {
+                quote!(style::CalcValue::LengthPercentage(#inner))
+            }
+        });
+    }
+}

+ 1150 - 0
packages/core-macro/src/styles/codegen.rs

@@ -0,0 +1,1150 @@
+use crate::*;
+use proc_macro2::TokenStream;
+use quote::{quote, ToTokens};
+
+macro_rules! path {
+    ($($t:tt)+) => {
+        ::quote::quote!(::style:: $($t)+)
+    };
+}
+
+impl ToTokens for DynamicStyles {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        let parts = self
+            .rules
+            .iter()
+            .filter(|style| !style.is_dummy())
+            .map(|style| style.to_token_stream());
+        tokens.extend(quote! {
+            {
+                let mut styles = style::Styles::new();
+                #(styles.push(#parts);)*
+                styles
+            }
+        })
+    }
+}
+
+impl ToTokens for DynamicStyle {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            DynamicStyle::Dynamic(block) => quote!(#block),
+            DynamicStyle::Literal(lit) => quote!(#lit),
+        })
+    }
+}
+
+impl ToTokens for Style {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        let path = quote!(::style::Style::);
+        tokens.extend(match self {
+            Style::Dummy => quote!(#path Dummy),
+            Style::Unchecked(v) => quote!(#path Unchecked(String::from(#v))),
+
+            Style::AlignContent(v) => quote!(#path AlignContent(#v)),
+            Style::AlignItems(v) => quote!(#path AlignItems(#v)),
+            Style::AlignSelf(v) => quote!(#path AlignSelf(#v)),
+            // all
+            // background
+            Style::BackgroundAttachment(v) => quote!(#path BackgroundAttachment(#v)),
+            Style::BackgroundBlendMode(v) => quote!(#path BackgroundBlendMode(#v)),
+            Style::BackgroundClip(v) => quote!(#path BackgroundClip(#v)),
+            Style::BackgroundColor(v) => quote!(#path BackgroundColor(#v)),
+            Style::BackgroundImage(v) => quote!(#path BackgroundImage(#v)),
+            Style::BackgroundOrigin(v) => quote!(#path BackgroundOrigin(#v)),
+            Style::BackgroundPosition(v) => quote!(#path BackgroundPosition(#v)),
+            Style::BackgroundRepeat(v) => quote!(#path BackgroundRepeat(#v)),
+            Style::BackgroundSize(v) => quote!(#path BackgroundSize(#v)),
+            Style::Border(v) => quote!(#path Border(#v)),
+            Style::BorderBottom(v) => quote!(#path BorderBottom(#v)),
+            Style::BorderBottomColor(v) => quote!(#path BorderBottomColor(#v)),
+            Style::BorderBottomLeftRadius(v) => quote!(#path BorderBottomLeftRadius(#v)),
+            Style::BorderBottomRightRadius(v) => quote!(#path BorderBottomRightRadius(#v)),
+            Style::BorderBottomStyle(v) => quote!(#path BorderBottomStyle(#v)),
+            Style::BorderBottomWidth(v) => quote!(#path BorderBottomWidth(#v)),
+            Style::BorderCollapse(v) => quote!(#path BorderCollapse(#v)),
+            Style::BorderColor(v) => quote!(#path BorderColor(#v)),
+            // border-image
+            // border-image-outset
+            // border-image-repeat
+            // border-image-slice
+            // border-image-source
+            // border-image-width
+            Style::BorderLeft(v) => quote!(#path BorderLeft(#v)),
+            Style::BorderLeftColor(v) => quote!(#path BorderLeftColor(#v)),
+            Style::BorderLeftStyle(v) => quote!(#path BorderLeftStyle(#v)),
+            Style::BorderLeftWidth(v) => quote!(#path BorderLeftWidth(#v)),
+            Style::BorderRadius(v) => quote!(#path BorderRadius(#v)),
+            Style::BorderRight(v) => quote!(#path BorderRight(#v)),
+            Style::BorderRightColor(v) => quote!(#path BorderRightColor(#v)),
+            Style::BorderRightStyle(v) => quote!(#path BorderRightStyle(#v)),
+            Style::BorderRightWidth(v) => quote!(#path BorderRightWidth(#v)),
+            // border-spacing
+            Style::BorderStyle(v) => quote!(#path BorderStyle(#v)),
+            Style::BorderTop(v) => quote!(#path BorderTop(#v)),
+            Style::BorderTopColor(v) => quote!(#path BorderTopColor(#v)),
+            Style::BorderTopLeftRadius(v) => quote!(#path BorderTopLeftRadius(#v)),
+            Style::BorderTopRightRadius(v) => quote!(#path BorderTopRightRadius(#v)),
+            Style::BorderTopStyle(v) => quote!(#path BorderTopStyle(#v)),
+            Style::BorderTopWidth(v) => quote!(#path BorderTopWidth(#v)),
+            Style::BorderWidth(v) => quote!(#path BorderWidth(#v)),
+            Style::Bottom(v) => quote!(#path Bottom(#v)),
+            // box-decoration-break
+            Style::BoxShadow(v) => quote!(#path BoxShadow(#v)),
+            Style::BoxSizing(v) => quote!(#path BoxSizing(#v)),
+            // break-after
+            // break-before
+            // break-inside
+            // caption-side
+            // caret-color
+            Style::Clear(v) => quote!(#path Clear(#v)),
+            // clip
+            // clip-path
+            // clip-rule
+            Style::ColumnCount(v) => quote!(#path ColumnCount(#v)),
+            Style::Color(v) => quote!(#path Color(#v)),
+            // contain
+            // content
+            // counter-increment
+            // counter-reset
+            // cue
+            // cue-after
+            // cue-before
+            Style::Cursor(v) => quote!(#path Cursor(#v)),
+            // direction
+            Style::Display(v) => quote!(#path Display(#v)),
+            // elevation
+            // empty-cells
+            // flex
+            Style::FlexBasis(v) => quote!(#path FlexBasis(#v)),
+            Style::FlexDirection(v) => quote!(#path FlexDirection(#v)),
+            // flex-flow
+            Style::FlexGrow(v) => quote!(#path FlexGrow(#v)),
+            Style::FlexShrink(v) => quote!(#path FlexShrink(#v)),
+            Style::FlexWrap(v) => quote!(#path FlexWrap(#v)),
+            Style::Float(v) => quote!(#path Float(#v)),
+            // font
+            Style::FontFamily(v) => quote!(#path FontFamily(#v)),
+            // font-feature-settings
+            // font-kerning
+            Style::FontSize(v) => quote!(#path FontSize(#v)),
+            // font-size-adjust
+            // font-stretch
+            Style::FontStyle(v) => quote!(#path FontStyle(#v)),
+            // font-synthesis
+            // font-variant
+            // font-variant-caps
+            // font-variant-east-asian
+            // font-variant-ligatures
+            // font-variant-numeric
+            // font-variant-position
+            Style::FontWeight(v) => quote!(#path FontWeight(#v)),
+            // glyph-orientation-vertical
+            // grid
+            // grid-area
+            // grid-auto-columns
+            // grid-auto-flow
+            // grid-auto-rows
+            // grid-column
+            // grid-column-end
+            // grid-column-start
+            // grid-row
+            // grid-row-end
+            // grid-row-start
+            // grid-template
+            // grid-template-areas
+            // grid-template-columns
+            // grid-template-rows
+            Style::Height(v) => quote!(#path Height(#v)),
+            // image-orientation
+            // image-rendering
+            // isolation
+            Style::JustifyContent(v) => quote!(#path JustifyContent(#v)),
+            Style::Left(v) => quote!(#path Left(#v)),
+            // letter-spacing
+            Style::LineHeight(v) => quote!(#path LineHeight(#v)),
+            // list-style
+            // list-style-image
+            // list-style-position
+            Style::ListStyleType(v) => quote!(#path ListStyleType(#v)),
+            Style::Margin(v) => quote!(#path Margin(#v)),
+            Style::MarginBottom(v) => quote!(#path MarginBottom(#v)),
+            Style::MarginLeft(v) => quote!(#path MarginLeft(#v)),
+            Style::MarginRight(v) => quote!(#path MarginRight(#v)),
+            Style::MarginTop(v) => quote!(#path MarginTop(#v)),
+            // mask
+            // mask-border
+            // mask-border-mode
+            // mask-border-outset
+            // mask-border-repeat
+            // mask-border-slice
+            // mask-border-source
+            // mask-border-width
+            // mask-clip
+            // mask-composite
+            // mask-image
+            // mask-mode
+            // mask-origin
+            // mask-position
+            // mask-repeat
+            // mask-size
+            // mask-type
+            Style::MaxHeight(v) => quote!(#path MaxHeight(#v)),
+            Style::MaxWidth(v) => quote!(#path MaxWidth(#v)),
+            Style::MinHeight(v) => quote!(#path MinHeight(#v)),
+            Style::MinWidth(v) => quote!(#path MinWidth(#v)),
+            // mix-blend-mode
+            Style::ObjectFit(v) => quote!(#path ObjectFit(#v)),
+            // object-position
+            // opacity
+            // order
+            // orphans
+            // outline
+            // outline-color
+            // outline-offset
+            // outline-style
+            // outline-width
+            Style::Overflow(v) => quote!(#path Overflow(#v)),
+            Style::OverflowX(v) => quote!(#path OverflowX(#v)),
+            Style::OverflowY(v) => quote!(#path OverflowY(#v)),
+            Style::Padding(v) => quote!(#path Padding(#v)),
+            Style::PaddingBottom(v) => quote!(#path PaddingBottom(#v)),
+            Style::PaddingLeft(v) => quote!(#path PaddingLeft(#v)),
+            Style::PaddingRight(v) => quote!(#path PaddingRight(#v)),
+            Style::PaddingTop(v) => quote!(#path PaddingTop(#v)),
+            // page-break-after
+            // page-break-before
+            // page-break-inside
+            // pause
+            // pause-after
+            // pause-before
+            // pitch
+            // pitch-range
+            // play-during
+            Style::Position(v) => quote!(#path Position(#v)),
+            // quotes
+            Style::Resize(v) => quote!(#path Resize(#v)),
+            // richness
+            Style::Right(v) => quote!(#path Right(#v)),
+            // scroll-margin
+            // scroll-margin-block
+            // scroll-margin-block-end
+            // scroll-margin-block-start
+            // scroll-margin-bottom
+            // scroll-margin-inline
+            // scroll-margin-inline-end
+            // scroll-margin-inline-start
+            // scroll-margin-left
+            // scroll-margin-right
+            // scroll-margin-top
+            // scroll-padding
+            // scroll-padding-block
+            // scroll-padding-block-end
+            // scroll-padding-block-start
+            // scroll-padding-bottom
+            // scroll-padding-inline
+            // scroll-padding-inline-end
+            // scroll-padding-inline-start
+            // scroll-padding-left
+            // scroll-padding-right
+            // scroll-padding-top
+            // scroll-snap-align
+            // scroll-snap-stop
+            // scroll-snap-type
+            // shape-image-threshold
+            // shape-margin
+            // shape-outside
+            // speak
+            // speak-header
+            // speak-numeral
+            // speak-punctuation
+            // speech-rate
+            // stress
+            // table-layout
+            Style::TextAlign(v) => quote!(#path TextAlign(#v)),
+            // text-combine-upright
+            // text-decoration
+            // text-decoration-color
+            // text-decoration-line
+            // text-decoration-style
+            // text-emphasis
+            // text-emphasis-color
+            // text-emphasis-position
+            // text-emphasis-style
+            // text-indent
+            // text-orientation
+            // text-overflow
+            // text-shadow
+            // text-transform
+            // text-underline-position
+            Style::Top(v) => quote!(#path Top(#v)),
+            // transform
+            // transform-box
+            // transform-origin
+            // unicode-bidi
+            // vertical-align
+            // visibility
+            // voice-family
+            // volume
+            // white-space
+            Style::WhiteSpace(v) => quote!(#path WhiteSpace(#v)),
+            Style::Widows(v) => quote!(#path Widows(#v)),
+            Style::Width(v) => quote!(#path Width(#v)),
+            // will-change
+            // word-spacing
+            // writing-mode
+            // z-index
+        });
+    }
+}
+
+impl ToTokens for AlignContent {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            AlignContent::FlexStart => path!(AlignContent::FlexStart),
+            AlignContent::Center => path!(AlignContent::Center),
+            AlignContent::FlexEnd => path!(style::AlignContent::FlexEnd),
+            AlignContent::SpaceAround => path!(AlignContent::SpaceAround),
+            AlignContent::SpaceBetween => path!(AlignContent::SpaceBetween),
+            AlignContent::Stretch => path!(AlignContent::Stretch),
+        });
+    }
+}
+
+impl ToTokens for Cursor {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            Cursor::Auto => path!(Cursor::Auto),
+            Cursor::Default => path!(Cursor::Default),
+            Cursor::None => path!(Cursor::None),
+            Cursor::ContextMenu => path!(Cursor::ContextMenu),
+            Cursor::Help => path!(Cursor::Help),
+            Cursor::Pointer => path!(Cursor::Pointer),
+            Cursor::Progress => path!(Cursor::Progress),
+            Cursor::Wait => path!(Cursor::Wait),
+            Cursor::Cell => path!(Cursor::Cell),
+            Cursor::Crosshair => path!(Cursor::Crosshair),
+            Cursor::Text => path!(Cursor::Text),
+            Cursor::VerticalText => path!(Cursor::VerticalText),
+            Cursor::Alias => path!(Cursor::Alias),
+            Cursor::Copy => path!(Cursor::Copy),
+            Cursor::Move => path!(Cursor::Move),
+            Cursor::NoDrop => path!(Cursor::NoDrop),
+            Cursor::NotAllowed => path!(Cursor::NotAllowed),
+            Cursor::Grab => path!(Cursor::Grab),
+            Cursor::Grabbing => path!(Cursor::Grabbing),
+            Cursor::EResize => path!(Cursor::EResize),
+            Cursor::NResize => path!(Cursor::NResize),
+            Cursor::NEResize => path!(Cursor::NEResize),
+            Cursor::NWResize => path!(Cursor::NWResize),
+            Cursor::SResize => path!(Cursor::SResize),
+            Cursor::SEResize => path!(Cursor::SEResize),
+            Cursor::SWResize => path!(Cursor::SWResize),
+            Cursor::WResize => path!(Cursor::WResize),
+            Cursor::EWResize => path!(Cursor::EWResize),
+            Cursor::NSResize => path!(Cursor::NSResize),
+            Cursor::NESWResize => path!(Cursor::NESWResize),
+            Cursor::NWSEResize => path!(Cursor::NWSEResize),
+            Cursor::ColResize => path!(Cursor::ColResize),
+            Cursor::RowResize => path!(Cursor::RowResize),
+            Cursor::AllScroll => path!(Cursor::AllScroll),
+            Cursor::ZoomIn => path!(Cursor::ZoomIn),
+            Cursor::ZoomOut => path!(Cursor::ZoomOut),
+        })
+    }
+}
+
+impl ToTokens for Display {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            Display::Block => path!(Display::Block),
+            Display::Flex => path!(Display::Flex),
+            Display::Inline => path!(Display::Inline),
+        });
+    }
+}
+
+impl ToTokens for FlexBasis {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            FlexBasis::Width(v) => path!(FlexBasis::Width(#v)),
+            FlexBasis::Content => path!(FlexBasis::Content),
+        });
+    }
+}
+
+impl ToTokens for FlexDirection {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            FlexDirection::Row => path!(FlexDirection::Row),
+            FlexDirection::Column => path!(FlexDirection::Column),
+        });
+    }
+}
+
+impl ToTokens for FlexWrap {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            FlexWrap::Wrap => path!(FlexWrap::Wrap),
+            FlexWrap::Nowrap => path!(FlexWrap::Nowrap),
+        });
+    }
+}
+
+impl ToTokens for AlignItems {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            AlignItems::Normal => path!(AlignItems::Normal),
+            AlignItems::Stretch => path!(AlignItems::Stretch),
+            AlignItems::Center => path!(AlignItems::Center),
+            AlignItems::Start => path!(AlignItems::Start),
+            AlignItems::End => path!(AlignItems::End),
+            AlignItems::FlexStart => path!(AlignItems::FlexStart),
+            AlignItems::FlexEnd => path!(AlignItems::FlexEnd),
+            AlignItems::Baseline => path!(AlignItems::Baseline),
+            AlignItems::FirstBaseline => path!(AlignItems::FirstBaseline),
+            AlignItems::LastBaseline => path!(AlignItems::LastBaseline),
+            AlignItems::SafeCenter => path!(AlignItems::SafeCenter),
+            AlignItems::UnsafeCenter => path!(AlignItems::UnsafeCenter),
+        });
+    }
+}
+
+impl ToTokens for AlignSelf {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            AlignSelf::Auto => path!(AlignSelf::Auto),
+            AlignSelf::Normal => path!(AlignSelf::Normal),
+            AlignSelf::Center => path!(AlignSelf::Center),
+            AlignSelf::Start => path!(AlignSelf::Start),
+            AlignSelf::End => path!(AlignSelf::End),
+            AlignSelf::SelfStart => path!(AlignSelf::SelfStart),
+            AlignSelf::SelfEnd => path!(AlignSelf::SelfEnd),
+            AlignSelf::FlexStart => path!(AlignSelf::FlexStart),
+            AlignSelf::FlexEnd => path!(AlignSelf::FlexEnd),
+            AlignSelf::Baseline => path!(AlignSelf::Baseline),
+            AlignSelf::FirstBaseline => path!(AlignSelf::FirstBaseline),
+            AlignSelf::LastBaseline => path!(AlignSelf::LastBaseline),
+            AlignSelf::Stretch => path!(AlignSelf::Stretch),
+            AlignSelf::SafeCenter => path!(AlignSelf::SafeCenter),
+            AlignSelf::UnsafeCenter => path!(AlignSelf::UnsafeCenter),
+        });
+    }
+}
+
+impl ToTokens for BackgroundAttachment {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            BackgroundAttachment::Scroll => path!(BackgroundAttachment::Scroll),
+            BackgroundAttachment::Fixed => path!(BackgroundAttachment::Fixed),
+            BackgroundAttachment::Local => path!(BackgroundAttachment::Local),
+        })
+    }
+}
+
+impl ToTokens for BlendMode {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            BlendMode::Normal => path!(BlendMode::Normal),
+            BlendMode::Multiply => path!(BlendMode::Multiply),
+            BlendMode::Screen => path!(BlendMode::Screen),
+            BlendMode::Overlay => path!(BlendMode::Overlay),
+            BlendMode::Darken => path!(BlendMode::Darken),
+            BlendMode::Lighten => path!(BlendMode::Lighten),
+            BlendMode::ColorDodge => path!(BlendMode::ColorDodge),
+            BlendMode::ColorBurn => path!(BlendMode::ColorBurn),
+            BlendMode::HardLight => path!(BlendMode::HardLight),
+            BlendMode::SoftLight => path!(BlendMode::SoftLight),
+            BlendMode::Difference => path!(BlendMode::Difference),
+            BlendMode::Exclusion => path!(BlendMode::Exclusion),
+            BlendMode::Hue => path!(BlendMode::Hue),
+            BlendMode::Saturation => path!(BlendMode::Saturation),
+            BlendMode::Color => path!(BlendMode::Color),
+            BlendMode::Luminosity => path!(BlendMode::Luminosity),
+        })
+    }
+}
+
+impl ToTokens for BackgroundBox {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            BackgroundBox::BorderBox => path!(BackgroundBox::BorderBox),
+            BackgroundBox::PaddingBox => path!(BackgroundBox::PaddingBox),
+            BackgroundBox::ContentBox => path!(BackgroundBox::ContentBox),
+        })
+    }
+}
+
+impl ToTokens for BackgroundImage {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            BackgroundImage::None => path!(BackgroundImage::None),
+            BackgroundImage::Url(url) => path!(BackgroundImage::Url(#url)),
+        })
+    }
+}
+
+impl ToTokens for BackgroundPosition {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            BackgroundPosition::Top => path!(BackgroundPosition::Top),
+            BackgroundPosition::Bottom => path!(BackgroundPosition::Bottom),
+            BackgroundPosition::Left => path!(BackgroundPosition::Left),
+            BackgroundPosition::Right => path!(BackgroundPosition::Right),
+            BackgroundPosition::Center => path!(BackgroundPosition::Center),
+        })
+    }
+}
+
+impl ToTokens for BackgroundRepeat {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            BackgroundRepeat::RepeatX => path!(BackgroundRepeat::RepeatX),
+            BackgroundRepeat::RepeatY => path!(BackgroundRepeat::RepeatY),
+            BackgroundRepeat::SingleOrDouble(v) => path!(BackgroundRepeat::SingleOrDouble(#v)),
+        })
+    }
+}
+
+impl ToTokens for BgRepeatPart {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            BgRepeatPart::Repeat => path!(BgRepeatPart::Repeat),
+            BgRepeatPart::Space => path!(BgRepeatPart::Space),
+            BgRepeatPart::Round => path!(BgRepeatPart::Round),
+            BgRepeatPart::NoRepeat => path!(BgRepeatPart::NoRepeat),
+        })
+    }
+}
+
+impl ToTokens for BackgroundSize {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            BackgroundSize::Cover => path!(BackgroundSize::Cover),
+            BackgroundSize::Contain => path!(BackgroundSize::Contain),
+            BackgroundSize::SingleOrDouble(v) => path!(BackgroundSize::SingleOrDouble(#v)),
+        })
+    }
+}
+
+impl ToTokens for BorderCollapse {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            BorderCollapse::Collapse => path!(BorderCollapse::Collapse),
+            BorderCollapse::Separate => path!(BorderCollapse::Separate),
+        })
+    }
+}
+
+impl ToTokens for JustifyContent {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            JustifyContent::FlexStart => path!(JustifyContent::FlexStart),
+            JustifyContent::Center => path!(JustifyContent::Center),
+            JustifyContent::FlexEnd => path!(JustifyContent::FlexEnd),
+            JustifyContent::SpaceAround => path!(JustifyContent::SpaceAround),
+            JustifyContent::SpaceBetween => path!(JustifyContent::SpaceBetween),
+        });
+    }
+}
+
+impl ToTokens for Float {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            Float::None => path!(Float::None),
+            Float::Left => path!(Float::Left),
+            Float::Right => path!(Float::Right),
+            Float::InlineStart => path!(Float::InlineStart),
+            Float::InlineEnd => path!(Float::InlineEnd),
+        })
+    }
+}
+
+impl ToTokens for FontWeight {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            FontWeight::Normal => path!(FontWeight::Normal),
+            FontWeight::Bold => path!(FontWeight::Bold),
+            FontWeight::Lighter => path!(FontWeight::Lighter),
+            FontWeight::Bolder => path!(FontWeight::Bolder),
+            FontWeight::Number(v) => path!(FontWeight::Number(#v)),
+        });
+    }
+}
+
+impl ToTokens for Font {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            Font::Named(inner) => path!(Font::Named(String::from(#inner))),
+            Font::Serif => path!(Font::Serif),
+            Font::SansSerif => path!(Font::SansSerif),
+            Font::Cursive => path!(Font::Cursive),
+            Font::Fantasy => path!(Font::Fantasy),
+            Font::Monospace => path!(Font::Monospace),
+        })
+    }
+}
+
+impl ToTokens for FontSize {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            FontSize::XXSmall => path!(FontSize::XXSmall),
+            FontSize::XSmall => path!(FontSize::XSmall),
+            FontSize::Small => path!(FontSize::Small),
+            FontSize::Medium => path!(FontSize::Medium),
+            FontSize::Large => path!(FontSize::Large),
+            FontSize::XLarge => path!(FontSize::XLarge),
+            FontSize::XXLarge => path!(FontSize::XXLarge),
+            FontSize::XXXLarge => path!(FontSize::XXXLarge),
+            FontSize::Larger => path!(FontSize::Larger),
+            FontSize::Smaller => path!(FontSize::Smaller),
+            FontSize::LengthPercentage(v) => path!(FontSize::LengthPercentage(#v)),
+        });
+    }
+}
+
+impl ToTokens for FontStyle {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            FontStyle::Normal => path!(FontStyle::Normal),
+            FontStyle::Italic => path!(FontStyle::Italic),
+            FontStyle::Oblique => path!(FontStyle::Oblique),
+        });
+    }
+}
+
+impl ToTokens for Border {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        let line_width = match self.line_width {
+            Some(line_width) => quote!(Some(#line_width)),
+            None => quote!(None),
+        };
+        let line_style = match self.line_style {
+            Some(line_style) => quote!(Some(#line_style)),
+            None => quote!(None),
+        };
+        let color = match self.color {
+            Some(color) => quote!(Some(#color)),
+            None => quote!(None),
+        };
+        tokens.extend(quote!(
+            style::Border {
+                line_width: #line_width,
+                line_style: #line_style,
+                color: #color,
+            }
+        ))
+    }
+}
+
+impl ToTokens for BoxShadow {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            BoxShadow::None => path!(BoxShadow::None),
+            BoxShadow::Shadows(list) => path!(BoxShadow::Shadows(#list)),
+        });
+    }
+}
+
+impl ToTokens for BoxSizing {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            BoxSizing::BorderBox => path!(BoxSizing::BorderBox),
+            BoxSizing::ContentBox => path!(BoxSizing::ContentBox),
+        });
+    }
+}
+
+impl ToTokens for Clear {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            Clear::None => path!(Clear::None),
+            Clear::Left => path!(Clear::Left),
+            Clear::Right => path!(Clear::Right),
+            Clear::Both => path!(Clear::Both),
+            Clear::InlineStart => path!(Clear::InlineStart),
+            Clear::InlineEnd => path!(Clear::InlineEnd),
+        })
+    }
+}
+
+impl ToTokens for ColumnCount {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            ColumnCount::Auto => path!(ColumnCount::Auto),
+            ColumnCount::Fixed(v) => path!(ColumnCount::Fixed(#v)),
+        })
+    }
+}
+
+impl ToTokens for Overflow {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            Overflow::Both(v) => path!(Overflow::Both(#v)),
+            Overflow::XY(x, y) => path!(Overflow::XY(#x, #y)),
+        })
+    }
+}
+
+impl ToTokens for OverflowXY {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            OverflowXY::Visible => path!(OverflowXY::Visible),
+            OverflowXY::Hidden => path!(OverflowXY::Hidden),
+            OverflowXY::Clip => path!(OverflowXY::Clip),
+            OverflowXY::Scroll => path!(OverflowXY::Scroll),
+            OverflowXY::Auto => path!(OverflowXY::Auto),
+        })
+    }
+}
+
+impl ToTokens for ObjectFit {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            ObjectFit::Fill => path!(ObjectFit::Fill),
+            ObjectFit::None => path!(ObjectFit::None),
+            ObjectFit::Contain { scale_down } => {
+                path!(ObjectFit::Contain { scale_down: #scale_down })
+            }
+            ObjectFit::Cover { scale_down } => path!(ObjectFit::Cover { scale_down: #scale_down }),
+        })
+    }
+}
+
+impl<T> ToTokens for Rect<T>
+where
+    T: ToTokens,
+{
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            Rect::All(v) => path!(Rect::All(#v)),
+            Rect::VerticalHorizontal(v, h) => path!(Rect::VerticalHorizontal(#v, #h)),
+            Rect::TopHorizontalBottom(t, h, b) => path!(Rect::TopHorizontalBottom(#t, #h, #b)),
+            Rect::TopRightBottomLeft(t, r, b, l) => path!(Rect::TopRightBottomLeft(#t, #r, #b, #l)),
+        });
+    }
+}
+
+impl ToTokens for LengthPercentage {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            LengthPercentage::Length(v) => path!(LengthPercentage::Length(#v)),
+            LengthPercentage::Percentage(v) => path!(LengthPercentage::Percentage(#v)),
+        });
+    }
+}
+
+impl ToTokens for AutoLengthPercentage {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            AutoLengthPercentage::LengthPercentage(v) => {
+                path!(AutoLengthPercentage::LengthPercentage(#v))
+            }
+            AutoLengthPercentage::Auto => path!(AutoLengthPercentage::Auto),
+        });
+    }
+}
+
+impl ToTokens for LineStyle {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            LineStyle::None => path!(LineStyle::None),
+            LineStyle::Hidden => path!(LineStyle::Hidden),
+            LineStyle::Dotted => path!(LineStyle::Dotted),
+            LineStyle::Dashed => path!(LineStyle::Dashed),
+            LineStyle::Solid => path!(LineStyle::Solid),
+            LineStyle::Double => path!(LineStyle::Double),
+            LineStyle::Groove => path!(LineStyle::Groove),
+            LineStyle::Ridge => path!(LineStyle::Ridge),
+            LineStyle::Inset => path!(LineStyle::Inset),
+            LineStyle::Outset => path!(LineStyle::Outset),
+        })
+    }
+}
+
+impl ToTokens for LineWidth {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            LineWidth::Length(length) => path!(LineWidth::Length(#length)),
+            LineWidth::Thin => path!(LineWidth::Thin),
+            LineWidth::Medium => path!(LineWidth::Medium),
+            LineWidth::Thick => path!(LineWidth::Thick),
+        })
+    }
+}
+
+impl ToTokens for LineHeight {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        self.0.to_tokens(tokens)
+    }
+}
+
+impl ToTokens for ListStyleType {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            ListStyleType::Disc => path!(ListStyleType::Disc),
+            ListStyleType::Circle => path!(ListStyleType::Circle),
+            ListStyleType::Square => path!(ListStyleType::Square),
+            ListStyleType::Decimal => path!(ListStyleType::Decimal),
+            ListStyleType::DecimalLeadingZero => path!(ListStyleType::DecimalLeadingZero),
+            ListStyleType::LowerRoman => path!(ListStyleType::LowerRoman),
+            ListStyleType::UpperRoman => path!(ListStyleType::UpperRoman),
+            ListStyleType::LowerGreek => path!(ListStyleType::LowerGreek),
+            ListStyleType::UpperGreek => path!(ListStyleType::UpperGreek),
+            ListStyleType::LowerLatin => path!(ListStyleType::LowerLatin),
+            ListStyleType::UpperLatin => path!(ListStyleType::UpperLatin),
+            ListStyleType::Armenian => path!(ListStyleType::Armenian),
+            ListStyleType::Georgian => path!(ListStyleType::Georgian),
+            ListStyleType::LowerAlpha => path!(ListStyleType::LowerAlpha),
+            ListStyleType::UpperAlpha => path!(ListStyleType::UpperAlpha),
+            ListStyleType::None => path!(ListStyleType::None),
+        })
+    }
+}
+
+impl ToTokens for Position {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            Position::Static => path!(Position::Static),
+            Position::Relative => path!(Position::Relative),
+            Position::Absolute => path!(Position::Absolute),
+            Position::Fixed => path!(Position::Fixed),
+        })
+    }
+}
+
+impl ToTokens for Resize {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            Resize::None => path!(Resize::None),
+            Resize::Both => path!(Resize::Both),
+            Resize::Horizontal => path!(Resize::Horizontal),
+            Resize::Vertical => path!(Resize::Vertical),
+        })
+    }
+}
+
+impl ToTokens for WhiteSpace {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            WhiteSpace::Normal => path!(WhiteSpace::Normal),
+            WhiteSpace::Pre => path!(WhiteSpace::Pre),
+            WhiteSpace::Nowrap => path!(WhiteSpace::Nowrap),
+            WhiteSpace::PreWrap => path!(WhiteSpace::PreWrap),
+            WhiteSpace::PreLine => path!(WhiteSpace::PreLine),
+        })
+    }
+}
+
+impl ToTokens for WidthHeight {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            WidthHeight::Auto => path!(WidthHeight::Auto),
+            WidthHeight::LengthPercentage(v) => path!(WidthHeight::LengthPercentage(#v)),
+            WidthHeight::MinContent => path!(WidthHeight::MinContent),
+            WidthHeight::MaxContent => path!(WidthHeight::MaxContent),
+            WidthHeight::FitContent(v) => path!(WidthHeight::FitContent(#v)),
+        })
+    }
+}
+
+impl ToTokens for MaxWidthHeight {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            MaxWidthHeight::None => path!(MaxWidthHeight::None),
+            MaxWidthHeight::LengthPercentage(v) => path!(MaxWidthHeight::LengthPercentage(#v)),
+            MaxWidthHeight::MinContent => path!(MaxWidthHeight::MinContent),
+            MaxWidthHeight::MaxContent => path!(MaxWidthHeight::MaxContent),
+            MaxWidthHeight::FitContent(v) => path!(MaxWidthHeight::FitContent(#v)),
+        })
+    }
+}
+
+impl ToTokens for Width21 {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            Width21::Auto => path!(Width21::Auto),
+            Width21::LengthPercentage(v) => path!(Width21::LengthPercentage(#v)),
+        })
+    }
+}
+
+impl ToTokens for Shadow {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        let color = match self.color.as_ref() {
+            Some(color) => quote!(Some(#color)),
+            None => quote!(None),
+        };
+        let length = &self.length;
+        let inset = &self.inset;
+        tokens.extend(quote! {
+            style::Shadow {
+                color: #color,
+                length: #length,
+                inset: #inset,
+            }
+        })
+    }
+}
+
+impl ToTokens for ShadowLength {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            ShadowLength::Offsets {
+                vertical,
+                horizontal,
+            } => path!(ShadowLength::Offsets {
+                vertical: #vertical,
+                horizontal: #horizontal,
+            }),
+            ShadowLength::OffsetsBlur {
+                vertical,
+                horizontal,
+                blur,
+            } => path!(ShadowLength::OffsetsBlur {
+                vertical: #vertical,
+                horizontal: #horizontal,
+                blur: #blur,
+            }),
+            ShadowLength::OffsetsBlurSpread {
+                vertical,
+                horizontal,
+                blur,
+                spread,
+            } => path!(ShadowLength::Offsets {
+                vertical: #vertical,
+                horizontal: #horizontal,
+                blur: #blur,
+                spread: #spread,
+            }),
+        })
+    }
+}
+
+impl ToTokens for TextAlign {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            TextAlign::Left => path!(TextAlign::Left),
+            TextAlign::Right => path!(TextAlign::Right),
+            TextAlign::Center => path!(TextAlign::Center),
+            TextAlign::Justify => path!(TextAlign::Justify),
+        });
+    }
+}
+
+impl ToTokens for Length {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            Length::Em(v) => path!(Length::Em(#v)),
+            Length::Ex(v) => path!(Length::Ex(#v)),
+            Length::In(v) => path!(Length::In(#v)),
+            Length::Cm(v) => path!(Length::Cm(#v)),
+            Length::Mm(v) => path!(Length::Mm(#v)),
+            Length::Pt(v) => path!(Length::Pt(#v)),
+            Length::Pc(v) => path!(Length::Pc(#v)),
+            Length::Px(v) => path!(Length::Px(#v)),
+            Length::Zero => path!(Length::Zero),
+        })
+    }
+}
+
+impl ToTokens for Percentage {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        let val = self.0;
+        tokens.extend(path!(Percentage(#val)));
+    }
+}
+
+impl ToTokens for DynamicColor {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            DynamicColor::Dynamic(block) => path!(DynamicColor::Literal(#block)),
+            DynamicColor::Literal(color) => path!(DynamicColor::Literal(#color)),
+        })
+    }
+}
+
+impl ToTokens for Color {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            Color::HexRGB(r, g, b) => path!(Color::HexRGB(#r, #g, #b)),
+            Color::HexRGBA(r, g, b, a) => path!(Color::HexRGB(#r, #g, #b, #a)),
+            Color::HSL(h, s, l) => path!(Color::HSL(#h, #s, #l)),
+            Color::HSLA(h, s, l, a) => path!(Color::HSLA(#h, #s, #l, #a)),
+            Color::IndianRed => path!(Color::IndianRed),
+            Color::LightCoral => path!(Color::LightCoral),
+            Color::Salmon => path!(Color::Salmon),
+            Color::DarkSalmon => path!(Color::DarkSalmon),
+            Color::LightSalmon => path!(Color::LightSalmon),
+            Color::Crimson => path!(Color::Crimson),
+            Color::Red => path!(Color::Red),
+            Color::FireBrick => path!(Color::FireBrick),
+            Color::DarkRed => path!(Color::DarkRed),
+            Color::Pink => path!(Color::Pink),
+            Color::LightPink => path!(Color::LightPink),
+            Color::HotPink => path!(Color::HotPink),
+            Color::DeepPink => path!(Color::DeepPink),
+            Color::MediumVioletRed => path!(Color::MediumVioletRed),
+            Color::PaleVioletRed => path!(Color::PaleVioletRed),
+            Color::Coral => path!(Color::Coral),
+            Color::Tomato => path!(Color::Tomato),
+            Color::OrangeRed => path!(Color::OrangeRed),
+            Color::DarkOrange => path!(Color::DarkOrange),
+            Color::Orange => path!(Color::Orange),
+            Color::Gold => path!(Color::Gold),
+            Color::Yellow => path!(Color::Yellow),
+            Color::LightYellow => path!(Color::LightYellow),
+            Color::LemonChiffon => path!(Color::LemonChiffon),
+            Color::LightGoldenrodYellow => path!(Color::LightGoldenrodYellow),
+            Color::PapayaWhip => path!(Color::PapayaWhip),
+            Color::Moccasin => path!(Color::Moccasin),
+            Color::PeachPuff => path!(Color::PeachPuff),
+            Color::PaleGoldenrod => path!(Color::PaleGoldenrod),
+            Color::Khaki => path!(Color::Khaki),
+            Color::DarkKhaki => path!(Color::DarkKhaki),
+            Color::Lavender => path!(Color::Lavender),
+            Color::Thistle => path!(Color::Thistle),
+            Color::Plum => path!(Color::Plum),
+            Color::Violet => path!(Color::Violet),
+            Color::Orchid => path!(Color::Orchid),
+            Color::Fuchsia => path!(Color::Fuchsia),
+            Color::Magenta => path!(Color::Magenta),
+            Color::MediumOrchid => path!(Color::MediumOrchid),
+            Color::MediumPurple => path!(Color::MediumPurple),
+            Color::RebeccaPurple => path!(Color::RebeccaPurple),
+            Color::BlueViolet => path!(Color::BlueViolet),
+            Color::DarkViolet => path!(Color::DarkViolet),
+            Color::DarkOrchid => path!(Color::DarkOrchid),
+            Color::DarkMagenta => path!(Color::DarkMagenta),
+            Color::Purple => path!(Color::Purple),
+            Color::Indigo => path!(Color::Indigo),
+            Color::SlateBlue => path!(Color::SlateBlue),
+            Color::DarkSlateBlue => path!(Color::DarkSlateBlue),
+            Color::MediumSlateBlue => path!(Color::MediumSlateBlue),
+            Color::GreenYellow => path!(Color::GreenYellow),
+            Color::Chartreuse => path!(Color::Chartreuse),
+            Color::LawnGreen => path!(Color::LawnGreen),
+            Color::Lime => path!(Color::Lime),
+            Color::LimeGreen => path!(Color::LimeGreen),
+            Color::PaleGreen => path!(Color::PaleGreen),
+            Color::LightGreen => path!(Color::LightGreen),
+            Color::MediumSpringGreen => path!(Color::MediumSpringGreen),
+            Color::SpringGreen => path!(Color::SpringGreen),
+            Color::MediumSeaGreen => path!(Color::MediumSeaGreen),
+            Color::SeaGreen => path!(Color::SeaGreen),
+            Color::ForestGreen => path!(Color::ForestGreen),
+            Color::Green => path!(Color::Green),
+            Color::DarkGreen => path!(Color::DarkGreen),
+            Color::YellowGreen => path!(Color::YellowGreen),
+            Color::OliveDrab => path!(Color::OliveDrab),
+            Color::Olive => path!(Color::Olive),
+            Color::DarkOliveGreen => path!(Color::DarkOliveGreen),
+            Color::MediumAquamarine => path!(Color::MediumAquamarine),
+            Color::DarkSeaGreen => path!(Color::DarkSeaGreen),
+            Color::LightSeaGreen => path!(Color::LightSeaGreen),
+            Color::DarkCyan => path!(Color::DarkCyan),
+            Color::Teal => path!(Color::Teal),
+            Color::Aqua => path!(Color::Aqua),
+            Color::Cyan => path!(Color::Cyan),
+            Color::LightCyan => path!(Color::LightCyan),
+            Color::PaleTurquoise => path!(Color::PaleTurquoise),
+            Color::Aquamarine => path!(Color::Aquamarine),
+            Color::Turquoise => path!(Color::Turquoise),
+            Color::MediumTurquoise => path!(Color::MediumTurquoise),
+            Color::DarkTurquoise => path!(Color::DarkTurquoise),
+            Color::CadetBlue => path!(Color::CadetBlue),
+            Color::SteelBlue => path!(Color::SteelBlue),
+            Color::LightSteelBlue => path!(Color::LightSteelBlue),
+            Color::PowderBlue => path!(Color::PowderBlue),
+            Color::LightBlue => path!(Color::LightBlue),
+            Color::SkyBlue => path!(Color::SkyBlue),
+            Color::LightSkyBlue => path!(Color::LightSkyBlue),
+            Color::DeepSkyBlue => path!(Color::DeepSkyBlue),
+            Color::DodgerBlue => path!(Color::DodgerBlue),
+            Color::CornflowerBlue => path!(Color::CornflowerBlue),
+            Color::RoyalBlue => path!(Color::RoyalBlue),
+            Color::Blue => path!(Color::Blue),
+            Color::MediumBlue => path!(Color::MediumBlue),
+            Color::DarkBlue => path!(Color::DarkBlue),
+            Color::Navy => path!(Color::Navy),
+            Color::MidnightBlue => path!(Color::MidnightBlue),
+            Color::Cornsilk => path!(Color::Cornsilk),
+            Color::BlanchedAlmond => path!(Color::BlanchedAlmond),
+            Color::Bisque => path!(Color::Bisque),
+            Color::NavajoWhite => path!(Color::NavajoWhite),
+            Color::Wheat => path!(Color::Wheat),
+            Color::BurlyWood => path!(Color::BurlyWood),
+            Color::Tan => path!(Color::Tan),
+            Color::RosyBrown => path!(Color::RosyBrown),
+            Color::SandyBrown => path!(Color::SandyBrown),
+            Color::Goldenrod => path!(Color::Goldenrod),
+            Color::DarkGoldenrod => path!(Color::DarkGoldenrod),
+            Color::Peru => path!(Color::Peru),
+            Color::Chocolate => path!(Color::Chocolate),
+            Color::SaddleBrown => path!(Color::SaddleBrown),
+            Color::Sienna => path!(Color::Sienna),
+            Color::Brown => path!(Color::Brown),
+            Color::Maroon => path!(Color::Maroon),
+            Color::White => path!(Color::White),
+            Color::Snow => path!(Color::Snow),
+            Color::HoneyDew => path!(Color::HoneyDew),
+            Color::MintCream => path!(Color::MintCream),
+            Color::Azure => path!(Color::Azure),
+            Color::AliceBlue => path!(Color::AliceBlue),
+            Color::GhostWhite => path!(Color::GhostWhite),
+            Color::WhiteSmoke => path!(Color::WhiteSmoke),
+            Color::SeaShell => path!(Color::SeaShell),
+            Color::Beige => path!(Color::Beige),
+            Color::OldLace => path!(Color::OldLace),
+            Color::FloralWhite => path!(Color::FloralWhite),
+            Color::Ivory => path!(Color::Ivory),
+            Color::AntiqueWhite => path!(Color::AntiqueWhite),
+            Color::Linen => path!(Color::Linen),
+            Color::LavenderBlush => path!(Color::LavenderBlush),
+            Color::MistyRose => path!(Color::MistyRose),
+            Color::Gainsboro => path!(Color::Gainsboro),
+            Color::LightGray => path!(Color::LightGray),
+            Color::Silver => path!(Color::Silver),
+            Color::DarkGray => path!(Color::DarkGray),
+            Color::Gray => path!(Color::Gray),
+            Color::DimGray => path!(Color::DimGray),
+            Color::LightSlateGray => path!(Color::LightSlateGray),
+            Color::SlateGray => path!(Color::SlateGray),
+            Color::DarkSlateGray => path!(Color::DarkSlateGray),
+            Color::Black => path!(Color::Black),
+        })
+    }
+}
+
+// Generic containers
+
+impl<T> ToTokens for NonemptyCommaList<T>
+where
+    T: ToTokens,
+{
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        let first = &self.first;
+        let rest = &self.rest;
+        tokens.extend(path! {
+            NonemptyCommaList {
+                first: #first,
+                rest: vec![#(#rest),*],
+            }
+        })
+    }
+}
+
+impl<T> ToTokens for SingleOrDouble<T>
+where
+    T: ToTokens,
+{
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            SingleOrDouble::Single(t) => path!(SingleOrDouble::Single(#t)),
+            SingleOrDouble::Double { vert, horiz } => path!(SingleOrDouble::Double {
+                vert: #vert,
+                horiz: #horiz,
+            }),
+        })
+    }
+}

+ 854 - 0
packages/core-macro/src/styles/color.rs

@@ -0,0 +1,854 @@
+use std::fmt;
+
+/// A color that possibly is possibly code, rather than a literal
+#[derive(Debug, Clone, PartialEq)]
+pub enum DynamicColor {
+    Literal(Color),
+    /// The type of the block is not checked here (it is checked by typeck).
+    Dynamic(syn::Block),
+}
+
+impl DynamicColor {
+    pub fn is_dynamic(&self) -> bool {
+        match self {
+            DynamicColor::Dynamic(_) => true,
+            DynamicColor::Literal(_) => false,
+        }
+    }
+}
+
+impl fmt::Display for DynamicColor {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            DynamicColor::Dynamic(_) => Ok(()),
+            DynamicColor::Literal(color) => color.fmt(f),
+        }
+    }
+}
+
+// TODO other color variants.
+#[derive(Debug, Clone, Copy, PartialEq)]
+#[non_exhaustive]
+pub enum Color {
+    HexRGB(u8, u8, u8),
+    HexRGBA(u8, u8, u8, u8),
+    // Invariants: `0 <= .0 < 360`, `0 <= .1 < 100`, `0 <= .2 < 100`.
+    HSL(f64, f64, f64),
+    // Invariants: `0 <= .0 < 360`, `0 <= .1 < 100`, `0 <= .2 < 100`, `0 <= .3 < 1`.
+    HSLA(f64, f64, f64, f64),
+
+    // Red HTML Color Names
+    /// rgb(205, 92, 92)
+    IndianRed,
+    /// rgb(240, 128, 128)
+    LightCoral,
+    /// rgb(250, 128, 114)
+    Salmon,
+    /// rgb(233, 150, 122)
+    DarkSalmon,
+    /// rgb(255, 160, 122)
+    LightSalmon,
+    /// rgb(220, 20, 60)
+    Crimson,
+    /// rgb(255, 0, 0)
+    Red,
+    /// rgb(178, 34, 34)
+    FireBrick,
+    /// rgb(139, 0, 0)
+    DarkRed,
+    // Pink HTML Color Names
+    /// rgb(255, 192, 203)
+    Pink,
+    /// rgb(255, 182, 193)
+    LightPink,
+    /// rgb(255, 105, 180)
+    HotPink,
+    /// rgb(255, 20, 147)
+    DeepPink,
+    /// rgb(199, 21, 133)
+    MediumVioletRed,
+    /// rgb(219, 112, 147)
+    PaleVioletRed,
+    //Orange HTML Color Names
+    // /// rgb(255, 160, 122) redefined
+    // LightSalmon,
+    /// rgb(255, 127, 80)
+    Coral,
+    /// rgb(255, 99, 71)
+    Tomato,
+    /// rgb(255, 69, 0)
+    OrangeRed,
+    /// rgb(255, 140, 0)
+    DarkOrange,
+    /// rgb(255, 165, 0)
+    Orange,
+    // Yellow HTML Color Names
+    /// rgb(255, 215, 0)
+    Gold,
+    /// rgb(255, 255, 0)
+    Yellow,
+    /// rgb(255, 255, 224)
+    LightYellow,
+    /// rgb(255, 250, 205)
+    LemonChiffon,
+    /// rgb(250, 250, 210)
+    LightGoldenrodYellow,
+    /// rgb(255, 239, 213)
+    PapayaWhip,
+    /// rgb(255, 228, 181)
+    Moccasin,
+    /// rgb(255, 218, 185)
+    PeachPuff,
+    /// rgb(238, 232, 170)
+    PaleGoldenrod,
+    /// rgb(240, 230, 140)
+    Khaki,
+    /// rgb(189, 183, 107)
+    DarkKhaki,
+    // Purple HTML Color Names
+    /// rgb(230, 230, 250)
+    Lavender,
+    /// rgb(216, 191, 216)
+    Thistle,
+    /// rgb(221, 160, 221)
+    Plum,
+    /// rgb(238, 130, 238)
+    Violet,
+    /// rgb(218, 112, 214)
+    Orchid,
+    /// rgb(255, 0, 255)
+    Fuchsia,
+    /// rgb(255, 0, 255)
+    Magenta,
+    /// rgb(186, 85, 211)
+    MediumOrchid,
+    /// rgb(147, 112, 219)
+    MediumPurple,
+    /// rgb(102, 51, 153)
+    RebeccaPurple,
+    /// rgb(138, 43, 226)
+    BlueViolet,
+    /// rgb(148, 0, 211)
+    DarkViolet,
+    /// rgb(153, 50, 204)
+    DarkOrchid,
+    /// rgb(139, 0, 139)
+    DarkMagenta,
+    /// rgb(128, 0, 128)
+    Purple,
+    /// rgb(75, 0, 130)
+    Indigo,
+    /// rgb(106, 90, 205)
+    SlateBlue,
+    /// rgb(72, 61, 139)
+    DarkSlateBlue,
+    /// rgb(123, 104, 238)
+    MediumSlateBlue,
+    // Green HTML Color Names
+    /// rgb(173, 255, 47)
+    GreenYellow,
+    /// rgb(127, 255, 0)
+    Chartreuse,
+    /// rgb(124, 252, 0)
+    LawnGreen,
+    /// rgb(0, 255, 0)
+    Lime,
+    /// rgb(50, 205, 50)
+    LimeGreen,
+    /// rgb(152, 251, 152)
+    PaleGreen,
+    /// rgb(144, 238, 144)
+    LightGreen,
+    /// rgb(0, 250, 154)
+    MediumSpringGreen,
+    /// rgb(0, 255, 127)
+    SpringGreen,
+    /// rgb(60, 179, 113)
+    MediumSeaGreen,
+    /// rgb(46, 139, 87)
+    SeaGreen,
+    /// rgb(34, 139, 34)
+    ForestGreen,
+    /// rgb(0, 128, 0)
+    Green,
+    /// rgb(0, 100, 0)
+    DarkGreen,
+    /// rgb(154, 205, 50)
+    YellowGreen,
+    /// rgb(107, 142, 35)
+    OliveDrab,
+    /// rgb(128, 128, 0)
+    Olive,
+    /// rgb(85, 107, 47)
+    DarkOliveGreen,
+    /// rgb(102, 205, 170)
+    MediumAquamarine,
+    /// rgb(143, 188, 139)
+    DarkSeaGreen,
+    /// rgb(32, 178, 170)
+    LightSeaGreen,
+    /// rgb(0, 139, 139)
+    DarkCyan,
+    /// rgb(0, 128, 128)
+    Teal,
+    // Blue HTML Color Names
+    /// rgb(0, 255, 255)
+    Aqua,
+    /// rgb(0, 255, 255)
+    Cyan,
+    /// rgb(224, 255, 255)
+    LightCyan,
+    /// rgb(175, 238, 238)
+    PaleTurquoise,
+    /// rgb(127, 255, 212)
+    Aquamarine,
+    /// rgb(64, 224, 208)
+    Turquoise,
+    /// rgb(72, 209, 204)
+    MediumTurquoise,
+    /// rgb(0, 206, 209)
+    DarkTurquoise,
+    /// rgb(95, 158, 160)
+    CadetBlue,
+    /// rgb(70, 130, 180)
+    SteelBlue,
+    /// rgb(176, 196, 222)
+    LightSteelBlue,
+    /// rgb(176, 224, 230)
+    PowderBlue,
+    /// rgb(173, 216, 230)
+    LightBlue,
+    /// rgb(135, 206, 235)
+    SkyBlue,
+    /// rgb(135, 206, 250)
+    LightSkyBlue,
+    /// rgb(0, 191, 255)
+    DeepSkyBlue,
+    /// rgb(30, 144, 255)
+    DodgerBlue,
+    /// rgb(100, 149, 237)
+    CornflowerBlue,
+    // /// rgb(123, 104, 238) duplicate
+    //MediumSlateBlue,
+    /// rgb(65, 105, 225)
+    RoyalBlue,
+    /// rgb(0, 0, 255)
+    Blue,
+    /// rgb(0, 0, 205)
+    MediumBlue,
+    /// rgb(0, 0, 139)
+    DarkBlue,
+    /// rgb(0, 0, 128)
+    Navy,
+    /// rgb(25, 25, 112)
+    MidnightBlue,
+    // Brown HTML Color Names
+    /// rgb(255, 248, 220)
+    Cornsilk,
+    /// rgb(255, 235, 205)
+    BlanchedAlmond,
+    /// rgb(255, 228, 196)
+    Bisque,
+    /// rgb(255, 222, 173)
+    NavajoWhite,
+    /// rgb(245, 222, 179)
+    Wheat,
+    /// rgb(222, 184, 135)
+    BurlyWood,
+    /// rgb(210, 180, 140)
+    Tan,
+    /// rgb(188, 143, 143)
+    RosyBrown,
+    /// rgb(244, 164, 96)
+    SandyBrown,
+    /// rgb(218, 165, 32)
+    Goldenrod,
+    /// rgb(184, 134, 11)
+    DarkGoldenrod,
+    /// rgb(205, 133, 63)
+    Peru,
+    /// rgb(210, 105, 30)
+    Chocolate,
+    /// rgb(139, 69, 19)
+    SaddleBrown,
+    /// rgb(160, 82, 45)
+    Sienna,
+    /// rgb(165, 42, 42)
+    Brown,
+    /// rgb(128, 0, 0)
+    Maroon,
+    // White HTML Color Names
+    /// rgb(255, 255, 255)
+    White,
+    /// rgb(255, 250, 250)
+    Snow,
+    /// rgb(240, 255, 240)
+    HoneyDew,
+    /// rgb(245, 255, 250)
+    MintCream,
+    /// rgb(240, 255, 255)
+    Azure,
+    /// rgb(240, 248, 255)
+    AliceBlue,
+    /// rgb(248, 248, 255)
+    GhostWhite,
+    /// rgb(245, 245, 245)
+    WhiteSmoke,
+    /// rgb(255, 245, 238)
+    SeaShell,
+    /// rgb(245, 245, 220)
+    Beige,
+    /// rgb(253, 245, 230)
+    OldLace,
+    /// rgb(255, 250, 240)
+    FloralWhite,
+    /// rgb(255, 255, 240)
+    Ivory,
+    /// rgb(250, 235, 215)
+    AntiqueWhite,
+    /// rgb(250, 240, 230)
+    Linen,
+    /// rgb(255, 240, 245)
+    LavenderBlush,
+    /// rgb(255, 228, 225)
+    MistyRose,
+    // Gray HTML Color Names
+    /// rgb(220, 220, 220)
+    Gainsboro,
+    /// rgb(211, 211, 211)
+    LightGray,
+    /// rgb(192, 192, 192)
+    Silver,
+    /// rgb(169, 169, 169)
+    DarkGray,
+    /// rgb(128, 128, 128)
+    Gray,
+    /// rgb(105, 105, 105)
+    DimGray,
+    /// rgb(119, 136, 153)
+    LightSlateGray,
+    /// rgb(112, 128, 144)
+    SlateGray,
+    /// rgb(47, 79, 79)
+    DarkSlateGray,
+    /// rgb(0, 0, 0)
+    Black,
+}
+
+impl Color {
+    // todo similar for others
+    pub fn to_rgb(self) -> Color {
+        use Color::*;
+        match self {
+            HexRGB(r, g, b) => HexRGB(r, g, b),
+            HexRGBA(r, g, b, _) => HexRGB(r, g, b),
+            HSL(h, s, l) => {
+                let s = s * 0.01; // percent conversion
+                let l = l * 0.01; // percent conversion
+                let (r, g, b) = hsl_to_rgb(h, s, l);
+                HexRGB((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
+            }
+            HSLA(h, s, l, _) => Color::to_rgb(HSL(h, s, l)),
+            IndianRed => HexRGB(205, 92, 92),
+            LightCoral => HexRGB(240, 128, 128),
+            Salmon => HexRGB(250, 128, 114),
+            DarkSalmon => HexRGB(233, 150, 122),
+            LightSalmon => HexRGB(255, 160, 122),
+            Crimson => HexRGB(220, 20, 60),
+            Red => HexRGB(255, 0, 0),
+            FireBrick => HexRGB(178, 34, 34),
+            DarkRed => HexRGB(139, 0, 0),
+            Pink => HexRGB(255, 192, 203),
+            LightPink => HexRGB(255, 182, 193),
+            HotPink => HexRGB(255, 105, 180),
+            DeepPink => HexRGB(255, 20, 147),
+            MediumVioletRed => HexRGB(199, 21, 133),
+            PaleVioletRed => HexRGB(219, 112, 147),
+            Coral => HexRGB(255, 127, 80),
+            Tomato => HexRGB(255, 99, 71),
+            OrangeRed => HexRGB(255, 69, 0),
+            DarkOrange => HexRGB(255, 140, 0),
+            Orange => HexRGB(255, 165, 0),
+            Gold => HexRGB(255, 215, 0),
+            Yellow => HexRGB(255, 255, 0),
+            LightYellow => HexRGB(255, 255, 224),
+            LemonChiffon => HexRGB(255, 250, 205),
+            LightGoldenrodYellow => HexRGB(250, 250, 210),
+            PapayaWhip => HexRGB(255, 239, 213),
+            Moccasin => HexRGB(255, 228, 181),
+            PeachPuff => HexRGB(255, 218, 185),
+            PaleGoldenrod => HexRGB(238, 232, 170),
+            Khaki => HexRGB(240, 230, 140),
+            DarkKhaki => HexRGB(189, 183, 107),
+            Lavender => HexRGB(230, 230, 250),
+            Thistle => HexRGB(216, 191, 216),
+            Plum => HexRGB(221, 160, 221),
+            Violet => HexRGB(238, 130, 238),
+            Orchid => HexRGB(218, 112, 214),
+            Fuchsia => HexRGB(255, 0, 255),
+            Magenta => HexRGB(255, 0, 255),
+            MediumOrchid => HexRGB(186, 85, 211),
+            MediumPurple => HexRGB(147, 112, 219),
+            RebeccaPurple => HexRGB(102, 51, 153),
+            BlueViolet => HexRGB(138, 43, 226),
+            DarkViolet => HexRGB(148, 0, 211),
+            DarkOrchid => HexRGB(153, 50, 204),
+            DarkMagenta => HexRGB(139, 0, 139),
+            Purple => HexRGB(128, 0, 128),
+            Indigo => HexRGB(75, 0, 130),
+            SlateBlue => HexRGB(106, 90, 205),
+            DarkSlateBlue => HexRGB(72, 61, 139),
+            MediumSlateBlue => HexRGB(123, 104, 238),
+            GreenYellow => HexRGB(173, 255, 47),
+            Chartreuse => HexRGB(127, 255, 0),
+            LawnGreen => HexRGB(124, 252, 0),
+            Lime => HexRGB(0, 255, 0),
+            LimeGreen => HexRGB(50, 205, 50),
+            PaleGreen => HexRGB(152, 251, 152),
+            LightGreen => HexRGB(144, 238, 144),
+            MediumSpringGreen => HexRGB(0, 250, 154),
+            SpringGreen => HexRGB(0, 255, 127),
+            MediumSeaGreen => HexRGB(60, 179, 113),
+            SeaGreen => HexRGB(46, 139, 87),
+            ForestGreen => HexRGB(34, 139, 34),
+            Green => HexRGB(0, 128, 0),
+            DarkGreen => HexRGB(0, 100, 0),
+            YellowGreen => HexRGB(154, 205, 50),
+            OliveDrab => HexRGB(107, 142, 35),
+            Olive => HexRGB(128, 128, 0),
+            DarkOliveGreen => HexRGB(85, 107, 47),
+            MediumAquamarine => HexRGB(102, 205, 170),
+            DarkSeaGreen => HexRGB(143, 188, 139),
+            LightSeaGreen => HexRGB(32, 178, 170),
+            DarkCyan => HexRGB(0, 139, 139),
+            Teal => HexRGB(0, 128, 128),
+            Aqua => HexRGB(0, 255, 255),
+            Cyan => HexRGB(0, 255, 255),
+            LightCyan => HexRGB(224, 255, 255),
+            PaleTurquoise => HexRGB(175, 238, 238),
+            Aquamarine => HexRGB(127, 255, 212),
+            Turquoise => HexRGB(64, 224, 208),
+            MediumTurquoise => HexRGB(72, 209, 204),
+            DarkTurquoise => HexRGB(0, 206, 209),
+            CadetBlue => HexRGB(95, 158, 160),
+            SteelBlue => HexRGB(70, 130, 180),
+            LightSteelBlue => HexRGB(176, 196, 222),
+            PowderBlue => HexRGB(176, 224, 230),
+            LightBlue => HexRGB(173, 216, 230),
+            SkyBlue => HexRGB(135, 206, 235),
+            LightSkyBlue => HexRGB(135, 206, 250),
+            DeepSkyBlue => HexRGB(0, 191, 255),
+            DodgerBlue => HexRGB(30, 144, 255),
+            CornflowerBlue => HexRGB(100, 149, 237),
+            RoyalBlue => HexRGB(65, 105, 225),
+            Blue => HexRGB(0, 0, 255),
+            MediumBlue => HexRGB(0, 0, 205),
+            DarkBlue => HexRGB(0, 0, 139),
+            Navy => HexRGB(0, 0, 128),
+            MidnightBlue => HexRGB(25, 25, 112),
+            Cornsilk => HexRGB(255, 248, 220),
+            BlanchedAlmond => HexRGB(255, 235, 205),
+            Bisque => HexRGB(255, 228, 196),
+            NavajoWhite => HexRGB(255, 222, 173),
+            Wheat => HexRGB(245, 222, 179),
+            BurlyWood => HexRGB(222, 184, 135),
+            Tan => HexRGB(210, 180, 140),
+            RosyBrown => HexRGB(188, 143, 143),
+            SandyBrown => HexRGB(244, 164, 96),
+            Goldenrod => HexRGB(218, 165, 32),
+            DarkGoldenrod => HexRGB(184, 134, 11),
+            Peru => HexRGB(205, 133, 63),
+            Chocolate => HexRGB(210, 105, 30),
+            SaddleBrown => HexRGB(139, 69, 19),
+            Sienna => HexRGB(160, 82, 45),
+            Brown => HexRGB(165, 42, 42),
+            Maroon => HexRGB(128, 0, 0),
+            White => HexRGB(255, 255, 255),
+            Snow => HexRGB(255, 250, 250),
+            HoneyDew => HexRGB(240, 255, 240),
+            MintCream => HexRGB(245, 255, 250),
+            Azure => HexRGB(240, 255, 255),
+            AliceBlue => HexRGB(240, 248, 255),
+            GhostWhite => HexRGB(248, 248, 255),
+            WhiteSmoke => HexRGB(245, 245, 245),
+            SeaShell => HexRGB(255, 245, 238),
+            Beige => HexRGB(245, 245, 220),
+            OldLace => HexRGB(253, 245, 230),
+            FloralWhite => HexRGB(255, 250, 240),
+            Ivory => HexRGB(255, 255, 240),
+            AntiqueWhite => HexRGB(250, 235, 215),
+            Linen => HexRGB(250, 240, 230),
+            LavenderBlush => HexRGB(255, 240, 245),
+            MistyRose => HexRGB(255, 228, 225),
+            Gainsboro => HexRGB(220, 220, 220),
+            LightGray => HexRGB(211, 211, 211),
+            Silver => HexRGB(192, 192, 192),
+            DarkGray => HexRGB(169, 169, 169),
+            Gray => HexRGB(128, 128, 128),
+            DimGray => HexRGB(105, 105, 105),
+            LightSlateGray => HexRGB(119, 136, 153),
+            SlateGray => HexRGB(112, 128, 144),
+            DarkSlateGray => HexRGB(47, 79, 79),
+            Black => HexRGB(0, 0, 0),
+        }
+    }
+
+    pub fn from_named(name: &str) -> Option<Self> {
+        // todo use a faster search (e.g. hashmap, aho-corasick)
+        use Color::*;
+        Some(match name {
+            "indianred" => IndianRed,
+            "lightcoral" => LightCoral,
+            "salmon" => Salmon,
+            "darksalmon" => DarkSalmon,
+            "lightsalmon" => LightSalmon,
+            "crimson" => Crimson,
+            "red" => Red,
+            "firebrick" => FireBrick,
+            "darkred" => DarkRed,
+            "pink" => Pink,
+            "lightpink" => LightPink,
+            "hotpink" => HotPink,
+            "deeppink" => DeepPink,
+            "mediumvioletred" => MediumVioletRed,
+            "palevioletred" => PaleVioletRed,
+            "coral" => Coral,
+            "tomato" => Tomato,
+            "orangered" => OrangeRed,
+            "darkorange" => DarkOrange,
+            "orange" => Orange,
+            "gold" => Gold,
+            "yellow" => Yellow,
+            "lightyellow" => LightYellow,
+            "lemonchiffon" => LemonChiffon,
+            "lightgoldenrodyellow" => LightGoldenrodYellow,
+            "papayawhip" => PapayaWhip,
+            "Moccasin" => Moccasin,
+            "Peachpuff" => PeachPuff,
+            "palegoldenrod" => PaleGoldenrod,
+            "khaki" => Khaki,
+            "darkkhaki" => DarkKhaki,
+            "lavender" => Lavender,
+            "thistle" => Thistle,
+            "plum" => Plum,
+            "violet" => Violet,
+            "orchid" => Orchid,
+            "fuchsia" => Fuchsia,
+            "magenta" => Magenta,
+            "mediumorchid" => MediumOrchid,
+            "mediumpurple" => MediumPurple,
+            "rebeccapurple" => RebeccaPurple,
+            "blueviolet" => BlueViolet,
+            "darkviolet" => DarkViolet,
+            "darkorchid" => DarkOrchid,
+            "darkmagenta" => DarkMagenta,
+            "purple" => Purple,
+            "indigo" => Indigo,
+            "slateblue" => SlateBlue,
+            "darkslateblue" => DarkSlateBlue,
+            "mediumslateblue" => MediumSlateBlue,
+            "greenyellow" => GreenYellow,
+            "chartreuse" => Chartreuse,
+            "lawngreen" => LawnGreen,
+            "lime" => Lime,
+            "limegreen" => LimeGreen,
+            "palegreen" => PaleGreen,
+            "lightgreen" => LightGreen,
+            "mediumspringgreen" => MediumSpringGreen,
+            "springgreen" => SpringGreen,
+            "mediumseagreen" => MediumSeaGreen,
+            "seagreen" => SeaGreen,
+            "forestgreen" => ForestGreen,
+            "green" => Green,
+            "darkgreen" => DarkGreen,
+            "yellowgreen" => YellowGreen,
+            "olivedrab" => OliveDrab,
+            "olive" => Olive,
+            "darkolivegreen" => DarkOliveGreen,
+            "mediumaquamarine" => MediumAquamarine,
+            "darkseagreen" => DarkSeaGreen,
+            "lightseagreen" => LightSeaGreen,
+            "darkcyan" => DarkCyan,
+            "teal" => Teal,
+            "aqua" => Aqua,
+            "cyan" => Cyan,
+            "lightcyan" => LightCyan,
+            "paleturquoise" => PaleTurquoise,
+            "aquamarine" => Aquamarine,
+            "turquoise" => Turquoise,
+            "mediumturquoise" => MediumTurquoise,
+            "darkturquoise" => DarkTurquoise,
+            "cadetblue" => CadetBlue,
+            "steelblue" => SteelBlue,
+            "lightsteelblue" => LightSteelBlue,
+            "powderblue" => PowderBlue,
+            "lightblue" => LightBlue,
+            "skyblue" => SkyBlue,
+            "lightskyblue" => LightSkyBlue,
+            "deepskyblue" => DeepSkyBlue,
+            "dodgerblue" => DodgerBlue,
+            "cornflowerblue" => CornflowerBlue,
+            "royalblue" => RoyalBlue,
+            "blue" => Blue,
+            "mediumblue" => MediumBlue,
+            "darkblue" => DarkBlue,
+            "navy" => Navy,
+            "midnightblue" => MidnightBlue,
+            "cornsilk" => Cornsilk,
+            "blanchedalmond" => BlanchedAlmond,
+            "bisque" => Bisque,
+            "navajowhite" => NavajoWhite,
+            "wheat" => Wheat,
+            "burlywood" => BurlyWood,
+            "tan" => Tan,
+            "rosybrown" => RosyBrown,
+            "sandybrown" => SandyBrown,
+            "goldenrod" => Goldenrod,
+            "darkgoldenrod" => DarkGoldenrod,
+            "peru" => Peru,
+            "chocolate" => Chocolate,
+            "saddlebrown" => SaddleBrown,
+            "sienna" => Sienna,
+            "brown" => Brown,
+            "maroon" => Maroon,
+            "white" => White,
+            "snow" => Snow,
+            "honeydew" => HoneyDew,
+            "mintcream" => MintCream,
+            "azure" => Azure,
+            "aliceblue" => AliceBlue,
+            "ghostwhite" => GhostWhite,
+            "whitesmoke" => WhiteSmoke,
+            "seashell" => SeaShell,
+            "beige" => Beige,
+            "oldlace" => OldLace,
+            "floralwhite" => FloralWhite,
+            "ivory" => Ivory,
+            "antiquewhite" => AntiqueWhite,
+            "linen" => Linen,
+            "lavenderblush" => LavenderBlush,
+            "mistyrose" => MistyRose,
+            "gainsboro" => Gainsboro,
+            "lightgray" => LightGray,
+            "silver" => Silver,
+            "darkgray" => DarkGray,
+            "gray" => Gray,
+            "dimgray" => DimGray,
+            "lightslategray" => LightSlateGray,
+            "slategray" => SlateGray,
+            "darkslategray" => DarkSlateGray,
+            "black" => Black,
+            _ => return None,
+        })
+    }
+}
+
+impl fmt::Display for Color {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        use Color::*;
+        match self {
+            HexRGB(r, g, b) => write!(f, "#{:02x}{:02x}{:02x}", r, g, b),
+            HexRGBA(r, g, b, a) => write!(f, "#{:02x}{:02x}{:02x}{:02x}", r, g, b, a),
+            HSL(h, s, l) => write!(f, "hsl({}, {}%, {}%)", h, s, l),
+            HSLA(h, s, l, a) => write!(f, "hsla({}, {}%, {}%, {})", h, s, l, a),
+            IndianRed => write!(f, "indianred"),
+            LightCoral => write!(f, "lightcoral"),
+            Salmon => write!(f, "salmon"),
+            DarkSalmon => write!(f, "darksalmon"),
+            LightSalmon => write!(f, "lightsalmon"),
+            Crimson => write!(f, "crimson"),
+            Red => write!(f, "red"),
+            FireBrick => write!(f, "firebrick"),
+            DarkRed => write!(f, "darkred"),
+            Pink => write!(f, "pink"),
+            LightPink => write!(f, "lightpink"),
+            HotPink => write!(f, "hotpink"),
+            DeepPink => write!(f, "deeppink"),
+            MediumVioletRed => write!(f, "mediumvioletred"),
+            PaleVioletRed => write!(f, "palevioletred"),
+            Coral => write!(f, "coral"),
+            Tomato => write!(f, "tomato"),
+            OrangeRed => write!(f, "orangered"),
+            DarkOrange => write!(f, "darkorange"),
+            Orange => write!(f, "orange"),
+            Gold => write!(f, "gold"),
+            Yellow => write!(f, "yellow"),
+            LightYellow => write!(f, "lightyellow"),
+            LemonChiffon => write!(f, "lemonchiffon"),
+            LightGoldenrodYellow => write!(f, "lightgoldenrodyellow"),
+            PapayaWhip => write!(f, "papayawhip"),
+            Moccasin => write!(f, "Moccasin"),
+            PeachPuff => write!(f, "Peachpuff"),
+            PaleGoldenrod => write!(f, "palegoldenrod"),
+            Khaki => write!(f, "khaki"),
+            DarkKhaki => write!(f, "darkkhaki"),
+            Lavender => write!(f, "lavender"),
+            Thistle => write!(f, "thistle"),
+            Plum => write!(f, "plum"),
+            Violet => write!(f, "violet"),
+            Orchid => write!(f, "orchid"),
+            Fuchsia => write!(f, "fuchsia"),
+            Magenta => write!(f, "magenta"),
+            MediumOrchid => write!(f, "mediumorchid"),
+            MediumPurple => write!(f, "mediumpurple"),
+            RebeccaPurple => write!(f, "rebeccapurple"),
+            BlueViolet => write!(f, "blueviolet"),
+            DarkViolet => write!(f, "darkviolet"),
+            DarkOrchid => write!(f, "darkorchid"),
+            DarkMagenta => write!(f, "darkmagenta"),
+            Purple => write!(f, "purple"),
+            Indigo => write!(f, "indigo"),
+            SlateBlue => write!(f, "slateblue"),
+            DarkSlateBlue => write!(f, "darkslateblue"),
+            MediumSlateBlue => write!(f, "mediumslateblue"),
+            GreenYellow => write!(f, "greenyellow"),
+            Chartreuse => write!(f, "chartreuse"),
+            LawnGreen => write!(f, "lawngreen"),
+            Lime => write!(f, "lime"),
+            LimeGreen => write!(f, "limegreen"),
+            PaleGreen => write!(f, "palegreen"),
+            LightGreen => write!(f, "lightgreen"),
+            MediumSpringGreen => write!(f, "mediumspringgreen"),
+            SpringGreen => write!(f, "springgreen"),
+            MediumSeaGreen => write!(f, "mediumseagreen"),
+            SeaGreen => write!(f, "seagreen"),
+            ForestGreen => write!(f, "forestgreen"),
+            Green => write!(f, "green"),
+            DarkGreen => write!(f, "darkgreen"),
+            YellowGreen => write!(f, "yellowgreen"),
+            OliveDrab => write!(f, "olivedrab"),
+            Olive => write!(f, "olive"),
+            DarkOliveGreen => write!(f, "darkolivegreen"),
+            MediumAquamarine => write!(f, "mediumaquamarine"),
+            DarkSeaGreen => write!(f, "darkseagreen"),
+            LightSeaGreen => write!(f, "lightseagreen"),
+            DarkCyan => write!(f, "darkcyan"),
+            Teal => write!(f, "teal"),
+            Aqua => write!(f, "aqua"),
+            Cyan => write!(f, "cyan"),
+            LightCyan => write!(f, "lightcyan"),
+            PaleTurquoise => write!(f, "paleturquoise"),
+            Aquamarine => write!(f, "aquamarine"),
+            Turquoise => write!(f, "turquoise"),
+            MediumTurquoise => write!(f, "mediumturquoise"),
+            DarkTurquoise => write!(f, "darkturquoise"),
+            CadetBlue => write!(f, "cadetblue"),
+            SteelBlue => write!(f, "steelblue"),
+            LightSteelBlue => write!(f, "lightsteelblue"),
+            PowderBlue => write!(f, "powderblue"),
+            LightBlue => write!(f, "lightblue"),
+            SkyBlue => write!(f, "skyblue"),
+            LightSkyBlue => write!(f, "lightskyblue"),
+            DeepSkyBlue => write!(f, "deepskyblue"),
+            DodgerBlue => write!(f, "dodgerblue"),
+            CornflowerBlue => write!(f, "cornflowerblue"),
+            RoyalBlue => write!(f, "royalblue"),
+            Blue => write!(f, "blue"),
+            MediumBlue => write!(f, "mediumblue"),
+            DarkBlue => write!(f, "darkblue"),
+            Navy => write!(f, "navy"),
+            MidnightBlue => write!(f, "midnightblue"),
+            Cornsilk => write!(f, "cornsilk"),
+            BlanchedAlmond => write!(f, "blanchedalmond"),
+            Bisque => write!(f, "bisque"),
+            NavajoWhite => write!(f, "navajowhite"),
+            Wheat => write!(f, "wheat"),
+            BurlyWood => write!(f, "burlywood"),
+            Tan => write!(f, "tan"),
+            RosyBrown => write!(f, "rosybrown"),
+            SandyBrown => write!(f, "sandybrown"),
+            Goldenrod => write!(f, "goldenrod"),
+            DarkGoldenrod => write!(f, "darkgoldenrod"),
+            Peru => write!(f, "peru"),
+            Chocolate => write!(f, "chocolate"),
+            SaddleBrown => write!(f, "saddlebrown"),
+            Sienna => write!(f, "sienna"),
+            Brown => write!(f, "brown"),
+            Maroon => write!(f, "maroon"),
+            White => write!(f, "white"),
+            Snow => write!(f, "snow"),
+            HoneyDew => write!(f, "honeydew"),
+            MintCream => write!(f, "mintcream"),
+            Azure => write!(f, "azure"),
+            AliceBlue => write!(f, "aliceblue"),
+            GhostWhite => write!(f, "ghostwhite"),
+            WhiteSmoke => write!(f, "whitesmoke"),
+            SeaShell => write!(f, "seashell"),
+            Beige => write!(f, "beige"),
+            OldLace => write!(f, "oldlace"),
+            FloralWhite => write!(f, "floralwhite"),
+            Ivory => write!(f, "ivory"),
+            AntiqueWhite => write!(f, "antiquewhite"),
+            Linen => write!(f, "linen"),
+            LavenderBlush => write!(f, "lavenderblush"),
+            MistyRose => write!(f, "mistyrose"),
+            Gainsboro => write!(f, "gainsboro"),
+            LightGray => write!(f, "lightgray"),
+            Silver => write!(f, "silver"),
+            DarkGray => write!(f, "darkgray"),
+            Gray => write!(f, "gray"),
+            DimGray => write!(f, "dimgray"),
+            LightSlateGray => write!(f, "lightslategray"),
+            SlateGray => write!(f, "slategray"),
+            DarkSlateGray => write!(f, "darkslategray"),
+            Black => write!(f, "black"),
+        }
+    }
+}
+
+fn hsl_to_rgb(h: f64, s: f64, l: f64) -> (f64, f64, f64) {
+    debug_assert!(h >= 0.0 && h < 360.0);
+    debug_assert!(s >= 0.0 && s <= 1.0);
+    debug_assert!(l >= 0.0 && l <= 1.0);
+    let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
+    let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
+    let m = l - c * 0.5;
+    let (rp, gp, bp) = if h < 60.0 {
+        (c, x, 0.0)
+    } else if h < 120.0 {
+        (x, c, 0.0)
+    } else if h < 180.0 {
+        (0.0, c, x)
+    } else if h < 240.0 {
+        (0.0, x, c)
+    } else if h < 300.0 {
+        (x, 0.0, c)
+    } else {
+        (c, 0.0, x)
+    };
+    (rp + m, gp + m, bp + m)
+}
+
+pub fn parse_hex(hex: &str) -> Option<Color> {
+    match hex.len() {
+        3 => {
+            let r = u8::from_str_radix(hex.get(0..1)?, 16).ok()?;
+            let g = u8::from_str_radix(hex.get(1..2)?, 16).ok()?;
+            let b = u8::from_str_radix(hex.get(2..3)?, 16).ok()?;
+            // #fff is equivalent to #ffffff
+            Some(Color::HexRGB(r << 4 | r, g << 4 | g, b << 4 | b))
+        }
+        6 => {
+            let r = u8::from_str_radix(hex.get(0..2)?, 16).ok()?;
+            let g = u8::from_str_radix(hex.get(2..4)?, 16).ok()?;
+            let b = u8::from_str_radix(hex.get(4..6)?, 16).ok()?;
+            Some(Color::HexRGB(r, g, b))
+        }
+        8 => {
+            let r = u8::from_str_radix(hex.get(0..2)?, 16).ok()?;
+            let g = u8::from_str_radix(hex.get(2..4)?, 16).ok()?;
+            let b = u8::from_str_radix(hex.get(4..6)?, 16).ok()?;
+            let a = u8::from_str_radix(hex.get(6..8)?, 16).ok()?;
+            Some(Color::HexRGBA(r, g, b, a))
+        }
+        _ => None,
+    }
+}
+
+#[test]
+fn test_color_convert() {
+    let color = Color::HSL(60.0, 0.0, 100.0);
+    assert_eq!(color.to_rgb(), Color::HexRGB(255, 255, 255));
+    let color = Color::HSL(0.0, 100.0, 50.0);
+    assert_eq!(color.to_rgb(), Color::HexRGB(255, 0, 0));
+}

+ 2044 - 0
packages/core-macro/src/styles/mod.rs

@@ -0,0 +1,2044 @@
+//! A module to type styles.
+// TODO most stuff here is on the stack, but there are a few heap-allocs here and there. It would
+// be good if we could just to allocate them in the bump arena when using bumpalo.
+mod calc;
+mod codegen;
+mod color;
+pub mod string;
+mod syn_parse;
+
+use std::{
+    fmt,
+    ops::{Deref, DerefMut},
+};
+
+pub use calc::*;
+pub use color::{Color, DynamicColor};
+// pub use crate::{
+//     calc::*,
+//     color::{Color, DynamicColor},
+// };
+
+pub struct DynamicStyles {
+    pub rules: Vec<DynamicStyle>,
+}
+
+impl From<Vec<DynamicStyle>> for DynamicStyles {
+    fn from(v: Vec<DynamicStyle>) -> Self {
+        Self { rules: v }
+    }
+}
+
+impl fmt::Display for DynamicStyles {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        for style in self
+            .rules
+            .iter()
+            .filter(|style| !(style.is_dynamic() || style.is_dummy()))
+        {
+            write!(f, "{};", style)?;
+        }
+        Ok(())
+    }
+}
+
+// TODO make container generic over heap (e.g. support bumpalo)
+#[derive(Debug, Clone, PartialEq)]
+pub struct Styles {
+    pub rules: Vec<Style>,
+}
+
+impl Styles {
+    pub fn new() -> Self {
+        Styles { rules: Vec::new() }
+    }
+
+    pub fn add(&mut self, style: Style) {
+        self.rules.push(style);
+    }
+
+    pub fn merge(&mut self, other: Styles) {
+        self.rules.extend(other.rules.into_iter())
+    }
+}
+
+impl From<DynamicStyles> for Styles {
+    fn from(dy: DynamicStyles) -> Self {
+        Styles {
+            rules: dy
+                .rules
+                .into_iter()
+                .filter_map(|dy_sty| match dy_sty {
+                    DynamicStyle::Dynamic(_) => None,
+                    DynamicStyle::Literal(l) => Some(l),
+                })
+                .collect(),
+        }
+    }
+}
+
+impl Deref for Styles {
+    type Target = Vec<Style>;
+    fn deref(&self) -> &Self::Target {
+        &self.rules
+    }
+}
+impl DerefMut for Styles {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.rules
+    }
+}
+
+impl From<Vec<Style>> for Styles {
+    fn from(v: Vec<Style>) -> Self {
+        Self { rules: v }
+    }
+}
+
+impl From<Styles> for Vec<Style> {
+    fn from(v: Styles) -> Self {
+        v.rules
+    }
+}
+
+impl fmt::Display for Styles {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        for style in self.rules.iter().filter(|style| !style.is_dummy()) {
+            write!(f, "{};", style)?;
+        }
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum DynamicStyle {
+    /// A literal style.
+    Literal(Style),
+    /// Tokens to pass through directly to codegen.
+    Dynamic(syn::Block),
+}
+
+impl DynamicStyle {
+    pub fn is_dynamic(&self) -> bool {
+        match self {
+            DynamicStyle::Literal(style) => style.is_dynamic(),
+            DynamicStyle::Dynamic(_) => true,
+        }
+    }
+    pub fn is_dummy(&self) -> bool {
+        match self {
+            DynamicStyle::Literal(style) => style.is_dummy(),
+            DynamicStyle::Dynamic(_) => false,
+        }
+    }
+}
+
+impl From<Style> for DynamicStyle {
+    fn from(style: Style) -> Self {
+        DynamicStyle::Literal(style)
+    }
+}
+
+impl fmt::Display for DynamicStyle {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            DynamicStyle::Literal(style) => style.fmt(f),
+            DynamicStyle::Dynamic(_) => Ok(()),
+        }
+    }
+}
+
+/// a `Style` is one of the css key/value pairs, also sometimes known as rules.
+#[derive(Debug, Clone, PartialEq)]
+#[non_exhaustive]
+pub enum Style {
+    /// For when you don't want to include any style at all (useful in expressions like `if`)
+    Dummy,
+    /// For when you want to use some unimplemented css. This is not type checked!
+    Unchecked(String),
+
+    // *From w3 spec:*
+    /// align-content
+    AlignContent(AlignContent),
+    /// align-items
+    AlignItems(AlignItems),
+    /// align-self
+    AlignSelf(AlignSelf),
+    // all - todo when doing global values
+    // background
+    /// background-attachment
+    BackgroundAttachment(BackgroundAttachment),
+    /// background-blend-mode
+    BackgroundBlendMode(NonemptyCommaList<BlendMode>),
+    /// background-clip
+    BackgroundClip(BackgroundBox),
+    /// background-color
+    BackgroundColor(DynamicColor),
+    /// background-image
+    BackgroundImage(NonemptyCommaList<BackgroundImage>),
+    /// background-origin
+    BackgroundOrigin(BackgroundBox),
+    /// background-position
+    BackgroundPosition(BackgroundPosition),
+    /// background-repeat
+    BackgroundRepeat(NonemptyCommaList<BackgroundRepeat>),
+    /// background-size
+    BackgroundSize(BackgroundSize),
+    /// border
+    Border(Border),
+    /// border-bottom
+    BorderBottom(Border),
+    /// border-bottom-color
+    BorderBottomColor(Color),
+    /// border-bottom-left-radius
+    BorderBottomLeftRadius(SingleOrDouble<LengthPercentage>),
+    /// border-bottom-right-radius
+    BorderBottomRightRadius(SingleOrDouble<LengthPercentage>),
+    /// border-bottom-style
+    BorderBottomStyle(LineStyle),
+    /// border-bottom-width
+    BorderBottomWidth(LineWidth),
+    /// border-collapse
+    BorderCollapse(BorderCollapse),
+    /// border-color
+    BorderColor(Rect<Color>),
+    // border-image
+    // border-image-outset
+    // border-image-repeat
+    // border-image-slice
+    // border-image-source
+    // border-image-width
+    /// border-left
+    BorderLeft(Border),
+    /// border-left-color
+    BorderLeftColor(Color),
+    /// border-left-style
+    BorderLeftStyle(LineStyle),
+    /// border-left-width
+    BorderLeftWidth(LineWidth),
+    /// border-radius
+    BorderRadius(BorderRadius),
+    /// border-right
+    BorderRight(Border),
+    /// border-right-color
+    BorderRightColor(Color),
+    /// border-right-style
+    BorderRightStyle(LineStyle),
+    /// border-right-width
+    BorderRightWidth(LineWidth),
+    // border-spacing
+    /// border-style
+    BorderStyle(BorderStyle),
+    /// border-top
+    BorderTop(Border),
+    /// border-top-color
+    BorderTopColor(Color),
+    /// border-top-left-radius
+    BorderTopLeftRadius(SingleOrDouble<LengthPercentage>),
+    /// border-top-right-radius
+    BorderTopRightRadius(SingleOrDouble<LengthPercentage>),
+    /// border-top-style
+    BorderTopStyle(LineStyle),
+    /// border-top-width
+    BorderTopWidth(LineWidth),
+    /// border-width
+    BorderWidth(BorderWidth),
+    /// bottom
+    Bottom(AutoLengthPercentage),
+    // box-decoration-break
+    /// box-shadow
+    BoxShadow(BoxShadow),
+    /// box-sizing
+    BoxSizing(BoxSizing),
+    // break-after
+    // break-before
+    // break-inside
+    // caption-side
+    // caret-color
+    /// clear
+    Clear(Clear),
+    // clip
+    // clip-path
+    // clip-rule
+    /// color
+    Color(DynamicColor),
+    /// column-count (manually added)
+    ColumnCount(ColumnCount),
+    // contain
+    // content
+    // counter-increment
+    // counter-reset
+    // cue
+    // cue-after
+    // cue-before
+    /// cursor
+    Cursor(Cursor),
+    // direction
+    /// display https://www.w3.org/TR/css-display-3/#typedef-display-outside
+    Display(Display),
+    // elevation
+    // empty-cells
+    // flex
+    /// flex-basis
+    FlexBasis(FlexBasis),
+    /// flex-direction
+    FlexDirection(FlexDirection),
+    // flex-flow
+    /// flex-grow
+    FlexGrow(f64),
+    /// flex-shrink
+    FlexShrink(f64),
+    /// flex-wrap
+    FlexWrap(FlexWrap),
+    /// float
+    Float(Float),
+    // font
+    /// font-family
+    FontFamily(FontFamily),
+    // font-feature-settings
+    // font-kerning
+    /// font-size
+    FontSize(FontSize),
+    // font-size-adjust
+    // font-stretch
+    /// font-style
+    FontStyle(FontStyle),
+    // font-synthesis
+    // font-variant
+    // font-variant-caps
+    // font-variant-east-asian
+    // font-variant-ligatures
+    // font-variant-numeric
+    // font-variant-position
+    /// font-weight
+    FontWeight(FontWeight),
+    // glyph-orientation-vertical
+    // grid
+    // grid-area
+    // grid-auto-columns
+    // grid-auto-flow
+    // grid-auto-rows
+    // grid-column
+    // grid-column-end
+    // grid-column-start
+    // grid-row
+    // grid-row-end
+    // grid-row-start
+    // grid-template
+    // grid-template-areas
+    // grid-template-columns
+    // grid-template-rows
+    /// height
+    Height(WidthHeight),
+    // image-orientation
+    // image-rendering
+    // isolation
+    /// justify-content
+    JustifyContent(JustifyContent),
+    /// left
+    Left(AutoLengthPercentage),
+    // letter-spacing
+    /// line-height
+    LineHeight(LineHeight),
+    // list-style
+    // list-style-image
+    // list-style-position
+    /// list-style-type
+    ListStyleType(ListStyleType),
+    /// margin
+    Margin(Margin),
+    /// margin-bottom
+    MarginBottom(MarginWidth),
+    /// margin-left
+    MarginLeft(MarginWidth),
+    /// margin-right
+    MarginRight(MarginWidth),
+    /// margin-top
+    MarginTop(MarginWidth),
+    // mask
+    // mask-border
+    // mask-border-mode
+    // mask-border-outset
+    // mask-border-repeat
+    // mask-border-slice
+    // mask-border-source
+    // mask-border-width
+    // mask-clip
+    // mask-composite
+    // mask-image
+    // mask-mode
+    // mask-origin
+    // mask-position
+    // mask-repeat
+    // mask-size
+    // mask-type
+    /// max-height
+    MaxHeight(MaxWidthHeight),
+    /// max-width
+    MaxWidth(MaxWidthHeight),
+    /// min-height - current implementing CSS2 spec
+    MinHeight(Calc),
+    /// min-width - current implementing CSS2 spec
+    MinWidth(Calc),
+    // mix-blend-mode
+    /// object-fit - https://drafts.csswg.org/css-images-4/#the-object-fit
+    ObjectFit(ObjectFit),
+    // object-position
+    // opacity
+    // order
+    // orphans
+    // outline
+    // outline-color
+    // outline-offset
+    // outline-style
+    // outline-width
+    /// overflow - https://drafts.csswg.org/css-overflow-3/#propdef-overflow
+    Overflow(Overflow),
+    /// overflow-x manually added
+    OverflowX(OverflowXY),
+    /// overflow-y manually added
+    OverflowY(OverflowXY),
+    /// padding
+    Padding(Padding),
+    /// padding-bottom
+    PaddingBottom(PaddingWidth),
+    /// padding-left
+    PaddingLeft(PaddingWidth),
+    /// padding-right
+    PaddingRight(PaddingWidth),
+    /// padding-top
+    PaddingTop(PaddingWidth),
+    // page-break-after
+    // page-break-before
+    // page-break-inside
+    // pause
+    // pause-after
+    // pause-before
+    // pitch
+    // pitch-range
+    // play-during
+    /// position
+    Position(Position),
+    // quotes
+    /// resize
+    Resize(Resize),
+    // richness
+    /// right
+    Right(AutoLengthPercentage),
+    // scroll-margin
+    // scroll-margin-block
+    // scroll-margin-block-end
+    // scroll-margin-block-start
+    // scroll-margin-bottom
+    // scroll-margin-inline
+    // scroll-margin-inline-end
+    // scroll-margin-inline-start
+    // scroll-margin-left
+    // scroll-margin-right
+    // scroll-margin-top
+    // scroll-padding
+    // scroll-padding-block
+    // scroll-padding-block-end
+    // scroll-padding-block-start
+    // scroll-padding-bottom
+    // scroll-padding-inline
+    // scroll-padding-inline-end
+    // scroll-padding-inline-start
+    // scroll-padding-left
+    // scroll-padding-right
+    // scroll-padding-top
+    // scroll-snap-align
+    // scroll-snap-stop
+    // scroll-snap-type
+    // shape-image-threshold
+    // shape-margin
+    // shape-outside
+    // speak
+    // speak-header
+    // speak-numeral
+    // speak-punctuation
+    // speech-rate
+    // stress
+    // table-layout
+    /// text-align
+    TextAlign(TextAlign),
+    // text-combine-upright
+    // text-decoration
+    // text-decoration-color
+    // text-decoration-line
+    // text-decoration-style
+    // text-emphasis
+    // text-emphasis-color
+    // text-emphasis-position
+    // text-emphasis-style
+    // text-indent
+    // text-orientation
+    // text-overflow
+    // text-shadow
+    // text-transform
+    // text-underline-position
+    /// top
+    Top(AutoLengthPercentage),
+    // transform
+    // transform-box
+    // transform-origin
+    // unicode-bidi
+    // vertical-align
+    // visibility
+    // voice-family
+    // volume
+    /// white-space
+    WhiteSpace(WhiteSpace),
+    /// widows
+    Widows(u32),
+    /// width
+    Width(WidthHeight),
+    // will-change
+    // word-spacing
+    // writing-mode
+    // z-index
+}
+
+impl Style {
+    fn is_dummy(&self) -> bool {
+        match self {
+            Style::Dummy => true,
+            _ => false,
+        }
+    }
+
+    fn is_dynamic(&self) -> bool {
+        match self {
+            Style::BackgroundColor(value) => value.is_dynamic(),
+            Style::Color(value) => value.is_dynamic(),
+            _ => false,
+        }
+    }
+}
+
+impl fmt::Display for Style {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Style::Dummy => Ok(()),
+            Style::Unchecked(v) => write!(f, "{}", v),
+
+            Style::AlignContent(v) => write!(f, "align-content:{}", v),
+            Style::AlignItems(v) => write!(f, "align-items:{}", v),
+            Style::AlignSelf(v) => write!(f, "align-self:{}", v),
+            // all - deferred
+            // background
+            Style::BackgroundAttachment(v) => write!(f, "background-attachment:{}", v),
+            Style::BackgroundBlendMode(v) => write!(f, "background-blend-mode:{}", v),
+            Style::BackgroundClip(v) => write!(f, "background-clip:{}", v),
+            Style::BackgroundColor(v) => write!(f, "background-color:{}", v),
+            Style::BackgroundImage(v) => write!(f, "background-image:{}", v),
+            Style::BackgroundOrigin(v) => write!(f, "background-origin:{}", v),
+            Style::BackgroundPosition(v) => write!(f, "background-position:{}", v),
+            Style::BackgroundRepeat(v) => write!(f, "background-repeat:{}", v),
+            Style::BackgroundSize(v) => write!(f, "background-size:{}", v),
+            Style::Border(v) => write!(f, "border:{}", v),
+            Style::BorderBottom(v) => write!(f, "border-bottom:{}", v),
+            Style::BorderBottomColor(v) => write!(f, "border-bottom-color:{}", v),
+            Style::BorderBottomLeftRadius(v) => write!(f, "border-bottom-left-radius:{}", v),
+            Style::BorderBottomRightRadius(v) => write!(f, "border-bottom-right-radius:{}", v),
+            Style::BorderBottomStyle(v) => write!(f, "border-bottom-style:{}", v),
+            Style::BorderBottomWidth(v) => write!(f, "border-bottom-width:{}", v),
+            Style::BorderCollapse(v) => write!(f, "border-collapse:{}", v),
+            Style::BorderColor(v) => write!(f, "border-color:{}", v),
+            // border-image
+            // border-image-outset
+            // border-image-repeat
+            // border-image-slice
+            // border-image-source
+            // border-image-width
+            Style::BorderLeft(v) => write!(f, "border-left:{}", v),
+            Style::BorderLeftColor(v) => write!(f, "border-left-color:{}", v),
+            Style::BorderLeftStyle(v) => write!(f, "border-left-style:{}", v),
+            Style::BorderLeftWidth(v) => write!(f, "border-left-width:{}", v),
+            Style::BorderRadius(v) => write!(f, "border-radius:{}", v),
+            Style::BorderRight(v) => write!(f, "border-right:{}", v),
+            Style::BorderRightColor(v) => write!(f, "border-right-color:{}", v),
+            Style::BorderRightStyle(v) => write!(f, "border-right-style:{}", v),
+            Style::BorderRightWidth(v) => write!(f, "border-right-width:{}", v),
+            // border-spacing
+            Style::BorderStyle(v) => write!(f, "border-style:{}", v),
+            Style::BorderTop(v) => write!(f, "border-top:{}", v),
+            Style::BorderTopColor(v) => write!(f, "border-top-color:{}", v),
+            Style::BorderTopLeftRadius(v) => write!(f, "border-top-left-radius:{}", v),
+            Style::BorderTopRightRadius(v) => write!(f, "border-top-right-radius:{}", v),
+            Style::BorderTopStyle(v) => write!(f, "border-top-style:{}", v),
+            Style::BorderTopWidth(v) => write!(f, "border-top-width:{}", v),
+            Style::BorderWidth(v) => write!(f, "border-width:{}", v),
+            Style::Bottom(v) => write!(f, "bottom:{}", v),
+            // box-decoration-break
+            Style::BoxShadow(v) => write!(f, "box-shadow:{}", v),
+            Style::BoxSizing(v) => write!(f, "box-sizing:{}", v),
+            // break-after
+            // break-before
+            // break-inside
+            // caption-side
+            // caret-color
+            Style::Clear(v) => write!(f, "clear:{}", v),
+            // clip
+            // clip-path
+            // clip-rule
+            Style::Color(v) => write!(f, "color:{}", v),
+            Style::ColumnCount(v) => write!(f, "column-count:{}", v),
+            // contain
+            // content
+            // counter-increment
+            // counter-reset
+            // cue
+            // cue-after
+            // cue-before
+            Style::Cursor(v) => write!(f, "cursor:{}", v),
+            // direction
+            Style::Display(v) => write!(f, "display:{}", v),
+            // elevation
+            // empty-cells
+            // flex
+            Style::FlexBasis(v) => write!(f, "flex-basis:{}", v),
+            Style::FlexDirection(v) => write!(f, "flex-direction:{}", v),
+            // flex-flow
+            Style::FlexGrow(v) => write!(f, "flex-grow:{}", v),
+            Style::FlexShrink(v) => write!(f, "flex-shrink:{}", v),
+            Style::FlexWrap(v) => write!(f, "flex-wrap:{}", v),
+            Style::Float(v) => write!(f, "float:{}", v),
+            // font
+            Style::FontFamily(v) => write!(f, "font-family:{}", v),
+            // font-feature-settings
+            // font-kerning
+            Style::FontSize(v) => write!(f, "font-size:{}", v),
+            // font-size-adjust
+            // font-stretch
+            Style::FontStyle(v) => write!(f, "font-style:{}", v),
+            // font-synthesis
+            // font-variant
+            // font-variant-caps
+            // font-variant-east-asian
+            // font-variant-ligatures
+            // font-variant-numeric
+            // font-variant-position
+            Style::FontWeight(v) => write!(f, "font-weight:{}", v),
+            // glyph-orientation-vertical
+            // grid
+            // grid-area
+            // grid-auto-columns
+            // grid-auto-flow
+            // grid-auto-rows
+            // grid-column
+            // grid-column-end
+            // grid-column-start
+            // grid-row
+            // grid-row-end
+            // grid-row-start
+            // grid-template
+            // grid-template-areas
+            // grid-template-columns
+            // grid-template-rows
+            Style::Height(v) => write!(f, "height:{}", v),
+            // image-orientation
+            // image-rendering
+            // isolation
+            Style::JustifyContent(v) => write!(f, "justify-content:{}", v),
+            // left
+            Style::Left(v) => write!(f, "left:{}", v),
+            // letter-spacing
+            // line-height
+            Style::LineHeight(v) => write!(f, "line-height:{}", v),
+            // list-style
+            // list-style-image
+            // list-style-position
+            Style::ListStyleType(v) => write!(f, "list-style-type:{}", v),
+            Style::Margin(v) => write!(f, "margin:{}", v),
+            Style::MarginBottom(v) => write!(f, "margin-bottom:{}", v),
+            Style::MarginLeft(v) => write!(f, "margin-left:{}", v),
+            Style::MarginRight(v) => write!(f, "margin-right:{}", v),
+            Style::MarginTop(v) => write!(f, "margin-top:{}", v),
+            // mask
+            // mask-border
+            // mask-border-mode
+            // mask-border-outset
+            // mask-border-repeat
+            // mask-border-slice
+            // mask-border-source
+            // mask-border-width
+            // mask-clip
+            // mask-composite
+            // mask-image
+            // mask-mode
+            // mask-origin
+            // mask-position
+            // mask-repeat
+            // mask-size
+            // mask-type
+            Style::MaxHeight(v) => write!(f, "max-height:{}", v),
+            Style::MaxWidth(v) => write!(f, "max-width:{}", v),
+            Style::MinHeight(v) => write!(f, "min-height:{}", v),
+            Style::MinWidth(v) => write!(f, "min-width:{}", v),
+            // mix-blend-mode
+            Style::ObjectFit(v) => write!(f, "object-fit:{}", v),
+            // object-position
+            // opacity
+            // order
+            // orphans
+            // outline
+            // outline-color
+            // outline-offset
+            // outline-style
+            // outline-width
+            Style::Overflow(v) => write!(f, "overflow:{}", v),
+            Style::OverflowX(v) => write!(f, "overflow-x:{}", v),
+            Style::OverflowY(v) => write!(f, "overflow-y:{}", v),
+            Style::Padding(v) => write!(f, "padding:{}", v),
+            Style::PaddingBottom(v) => write!(f, "padding-bottom:{}", v),
+            Style::PaddingLeft(v) => write!(f, "padding-left:{}", v),
+            Style::PaddingRight(v) => write!(f, "padding-right:{}", v),
+            Style::PaddingTop(v) => write!(f, "padding-top:{}", v),
+            // padding-bottom
+            // padding-left
+            // padding-right
+            // padding-top
+            // page-break-after
+            // page-break-before
+            // page-break-inside
+            // pause
+            // pause-after
+            // pause-before
+            // pitch
+            // pitch-range
+            // play-during
+            Style::Position(v) => write!(f, "position:{}", v),
+            // uotes
+            Style::Resize(v) => write!(f, "resize:{}", v),
+            // richness
+            Style::Right(v) => write!(f, "right:{}", v),
+            // scroll-margin
+            // scroll-margin-block
+            // scroll-margin-block-end
+            // scroll-margin-block-start
+            // scroll-margin-bottom
+            // scroll-margin-inline
+            // scroll-margin-inline-end
+            // scroll-margin-inline-start
+            // scroll-margin-left
+            // scroll-margin-right
+            // scroll-margin-top
+            // scroll-padding
+            // scroll-padding-block
+            // scroll-padding-block-end
+            // scroll-padding-block-start
+            // scroll-padding-bottom
+            // scroll-padding-inline
+            // scroll-padding-inline-end
+            // scroll-padding-inline-start
+            // scroll-padding-left
+            // scroll-padding-right
+            // scroll-padding-top
+            // scroll-snap-align
+            // scroll-snap-stop
+            // scroll-snap-type
+            // shape-image-threshold
+            // shape-margin
+            // shape-outside
+            // speak
+            // speak-header
+            // speak-numeral
+            // speak-punctuation
+            // speech-rate
+            // stress
+            // table-layout
+            Style::TextAlign(v) => write!(f, "text-align:{}", v),
+            // text-combine-upright
+            // text-decoration
+            // text-decoration-color
+            // text-decoration-line
+            // text-decoration-style
+            // text-emphasis
+            // text-emphasis-color
+            // text-emphasis-position
+            // text-emphasis-style
+            // text-indent
+            // text-orientation
+            // text-overflow
+            // text-shadow
+            // text-transform
+            // text-underline-position
+            // top
+            Style::Top(v) => write!(f, "top:{}", v),
+            // transform
+            // transform-box
+            // transform-origin
+            // unicode-bidi
+            // vertical-align
+            // visibility
+            // voice-family
+            // volume
+            Style::WhiteSpace(v) => write!(f, "white-space:{}", v),
+            Style::Widows(v) => write!(f, "widows:{}", v),
+            Style::Width(v) => write!(f, "width:{}", v),
+            // will-change
+            // word-spacing
+            // writing-mode
+            // z-index
+        }
+    }
+}
+
+/// https://www.w3.org/TR/css-flexbox-1/#propdef-justify-content
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum AlignContent {
+    FlexStart,
+    Center,
+    FlexEnd,
+    SpaceBetween,
+    SpaceAround,
+    Stretch,
+}
+
+impl Default for AlignContent {
+    fn default() -> Self {
+        AlignContent::Stretch
+    }
+}
+
+impl fmt::Display for AlignContent {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            AlignContent::FlexStart => write!(f, "flex-start"),
+            AlignContent::Center => write!(f, "center"),
+            AlignContent::FlexEnd => write!(f, "flex-end"),
+            AlignContent::SpaceAround => write!(f, "space-around"),
+            AlignContent::SpaceBetween => write!(f, "space-between"),
+            AlignContent::Stretch => write!(f, "stretch"),
+        }
+    }
+}
+
+/// https://developer.mozilla.org/en-US/docs/Web/CSS/align-items
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum AlignItems {
+    Normal,
+    Stretch,
+    Center,
+    Start,
+    End,
+    FlexStart,
+    FlexEnd,
+    Baseline,
+    FirstBaseline,
+    LastBaseline,
+    SafeCenter,
+    UnsafeCenter,
+}
+
+impl fmt::Display for AlignItems {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            AlignItems::Normal => write!(f, "normal"),
+            AlignItems::Stretch => write!(f, "stretch"),
+            AlignItems::Center => write!(f, "center"),
+            AlignItems::Start => write!(f, "start"),
+            AlignItems::End => write!(f, "end"),
+            AlignItems::FlexStart => write!(f, "flex-start"),
+            AlignItems::FlexEnd => write!(f, "flex-end"),
+            AlignItems::Baseline => write!(f, "baseline"),
+            AlignItems::FirstBaseline => write!(f, "first baseline"),
+            AlignItems::LastBaseline => write!(f, "last baseline"),
+            AlignItems::SafeCenter => write!(f, "safe center"),
+            AlignItems::UnsafeCenter => write!(f, "unsafe center"),
+        }
+    }
+}
+
+/// https://developer.mozilla.org/en-US/docs/Web/CSS/align-self
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum AlignSelf {
+    Auto,
+    Normal,
+    Center,
+    Start,
+    End,
+    SelfStart,
+    SelfEnd,
+    FlexStart,
+    FlexEnd,
+    Baseline,
+    FirstBaseline,
+    LastBaseline,
+    Stretch,
+    SafeCenter,
+    UnsafeCenter,
+}
+
+impl fmt::Display for AlignSelf {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            AlignSelf::Auto => write!(f, "auto"),
+            AlignSelf::Normal => write!(f, "normal"),
+            AlignSelf::Center => write!(f, "center"),
+            AlignSelf::Start => write!(f, "start"),
+            AlignSelf::End => write!(f, "end"),
+            AlignSelf::SelfStart => write!(f, "self-start"),
+            AlignSelf::SelfEnd => write!(f, "self-end"),
+            AlignSelf::FlexStart => write!(f, "flex-start"),
+            AlignSelf::FlexEnd => write!(f, "flex-end"),
+            AlignSelf::Baseline => write!(f, "baseline"),
+            AlignSelf::FirstBaseline => write!(f, "first baseline"),
+            AlignSelf::LastBaseline => write!(f, "last baseline"),
+            AlignSelf::Stretch => write!(f, "stretch"),
+            AlignSelf::SafeCenter => write!(f, "safe center"),
+            AlignSelf::UnsafeCenter => write!(f, "unsafe center"),
+        }
+    }
+}
+
+/// https://developer.mozilla.org/en-US/docs/Web/CSS/background-attachment
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum BackgroundAttachment {
+    Scroll,
+    Fixed,
+    Local,
+}
+
+impl fmt::Display for BackgroundAttachment {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            BackgroundAttachment::Scroll => write!(f, "scroll"),
+            BackgroundAttachment::Fixed => write!(f, "fixed"),
+            BackgroundAttachment::Local => write!(f, "local"),
+        }
+    }
+}
+
+/// https://developer.mozilla.org/en-US/docs/Web/CSS/background-blend-mode
+#[derive(Debug, Clone, PartialEq)]
+pub enum BlendMode {
+    Normal,
+    Multiply,
+    Screen,
+    Overlay,
+    Darken,
+    Lighten,
+    ColorDodge,
+    ColorBurn,
+    HardLight,
+    SoftLight,
+    Difference,
+    Exclusion,
+    Hue,
+    Saturation,
+    Color,
+    Luminosity,
+}
+
+impl fmt::Display for BlendMode {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            BlendMode::Normal => write!(f, "normal"),
+            BlendMode::Multiply => write!(f, "multiply"),
+            BlendMode::Screen => write!(f, "screen"),
+            BlendMode::Overlay => write!(f, "overlay"),
+            BlendMode::Darken => write!(f, "darken"),
+            BlendMode::Lighten => write!(f, "lighten"),
+            BlendMode::ColorDodge => write!(f, "color-dodge"),
+            BlendMode::ColorBurn => write!(f, "color-burn"),
+            BlendMode::HardLight => write!(f, "hard-light"),
+            BlendMode::SoftLight => write!(f, "soft-light"),
+            BlendMode::Difference => write!(f, "difference"),
+            BlendMode::Exclusion => write!(f, "exclusion"),
+            BlendMode::Hue => write!(f, "hue"),
+            BlendMode::Saturation => write!(f, "saturation"),
+            BlendMode::Color => write!(f, "color"),
+            BlendMode::Luminosity => write!(f, "luminosity"),
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum BackgroundBox {
+    BorderBox,
+    PaddingBox,
+    ContentBox,
+}
+
+impl fmt::Display for BackgroundBox {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            BackgroundBox::BorderBox => write!(f, "border-box"),
+            BackgroundBox::PaddingBox => write!(f, "padding-box"),
+            BackgroundBox::ContentBox => write!(f, "content-box"),
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum BackgroundImage {
+    None,
+    Url(String),
+    // TODO other variants
+}
+
+impl fmt::Display for BackgroundImage {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            BackgroundImage::None => write!(f, "none"),
+            BackgroundImage::Url(url) => write!(f, "url({})", url),
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum BackgroundPosition {
+    Top,
+    Bottom,
+    Left,
+    Right,
+    Center,
+    // TODO other variants
+}
+
+impl fmt::Display for BackgroundPosition {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            BackgroundPosition::Top => write!(f, "top"),
+            BackgroundPosition::Left => write!(f, "left"),
+            BackgroundPosition::Bottom => write!(f, "bottom"),
+            BackgroundPosition::Right => write!(f, "right"),
+            BackgroundPosition::Center => write!(f, "center"),
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum BackgroundRepeat {
+    RepeatX,
+    RepeatY,
+    SingleOrDouble(SingleOrDouble<BgRepeatPart>),
+}
+
+impl fmt::Display for BackgroundRepeat {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            BackgroundRepeat::RepeatX => write!(f, "repeat-x"),
+            BackgroundRepeat::RepeatY => write!(f, "repeat-y"),
+            BackgroundRepeat::SingleOrDouble(v) => v.fmt(f),
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum BgRepeatPart {
+    Repeat,
+    Space,
+    Round,
+    NoRepeat,
+}
+
+impl fmt::Display for BgRepeatPart {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            BgRepeatPart::Repeat => write!(f, "repeat"),
+            BgRepeatPart::Space => write!(f, "space"),
+            BgRepeatPart::Round => write!(f, "round"),
+            BgRepeatPart::NoRepeat => write!(f, "no-repeat"),
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum BackgroundSize {
+    Cover,
+    Contain,
+    SingleOrDouble(SingleOrDouble<AutoLengthPercentage>),
+}
+
+impl fmt::Display for BackgroundSize {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            BackgroundSize::Cover => write!(f, "cover"),
+            BackgroundSize::Contain => write!(f, "contain"),
+            BackgroundSize::SingleOrDouble(v) => v.fmt(f),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Border {
+    pub line_width: Option<LineWidth>,
+    pub line_style: Option<LineStyle>,
+    pub color: Option<Color>,
+}
+
+impl Border {
+    fn new() -> Self {
+        Border {
+            line_width: None,
+            line_style: None,
+            color: None,
+        }
+    }
+
+    fn is_full(&self) -> bool {
+        match (&self.line_width, &self.line_style, &self.color) {
+            (Some(_), Some(_), Some(_)) => true,
+            _ => false,
+        }
+    }
+
+    fn has_line_width(&self) -> bool {
+        self.line_width.is_some()
+    }
+    fn has_line_style(&self) -> bool {
+        self.line_style.is_some()
+    }
+    fn has_color(&self) -> bool {
+        self.color.is_some()
+    }
+}
+
+impl fmt::Display for Border {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        fn space(yes: bool) -> &'static str {
+            if yes {
+                " "
+            } else {
+                ""
+            }
+        }
+        let mut cont = false;
+        if let Some(line_width) = self.line_width {
+            write!(f, "{}", line_width)?;
+            cont = true;
+        }
+        if let Some(line_style) = self.line_style {
+            write!(f, "{}{}", space(cont), line_style)?;
+            cont = true;
+        }
+        if let Some(color) = self.color {
+            write!(f, "{}{}", space(cont), color)?;
+        }
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum BorderCollapse {
+    Collapse,
+    Separate,
+}
+
+impl fmt::Display for BorderCollapse {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            BorderCollapse::Collapse => write!(f, "collapse"),
+            BorderCollapse::Separate => write!(f, "separate"),
+        }
+    }
+}
+
+pub type BorderRadius = Calc;
+
+pub type BorderStyle = Rect<LineStyle>;
+
+pub type BorderWidth = Rect<LineWidth>;
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum BoxShadow {
+    None,
+    Shadows(NonemptyCommaList<Shadow>),
+}
+
+impl fmt::Display for BoxShadow {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            BoxShadow::None => f.write_str("none"),
+            BoxShadow::Shadows(list) => write!(f, "{}", list),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum BoxSizing {
+    BorderBox,
+    ContentBox,
+}
+
+impl fmt::Display for BoxSizing {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            BoxSizing::BorderBox => f.write_str("border-box"),
+            BoxSizing::ContentBox => f.write_str("content-box"),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum Clear {
+    None,
+    Left,
+    Right,
+    Both,
+    InlineStart,
+    InlineEnd,
+}
+
+impl fmt::Display for Clear {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Clear::None => f.write_str("none"),
+            Clear::Left => f.write_str("left"),
+            Clear::Right => f.write_str("right"),
+            Clear::Both => f.write_str("both"),
+            Clear::InlineStart => f.write_str("inline-start"),
+            Clear::InlineEnd => f.write_str("inline-end"),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum ColumnCount {
+    Auto,
+    Fixed(u32),
+}
+
+impl fmt::Display for ColumnCount {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            ColumnCount::Auto => f.write_str("auto"),
+            ColumnCount::Fixed(inner) => write!(f, "{}", inner),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum Cursor {
+    // todo url
+    Auto,
+    Default,
+    None,
+    ContextMenu,
+    Help,
+    Pointer,
+    Progress,
+    Wait,
+    Cell,
+    Crosshair,
+    Text,
+    VerticalText,
+    Alias,
+    Copy,
+    Move,
+    NoDrop,
+    NotAllowed,
+    Grab,
+    Grabbing,
+    EResize,
+    NResize,
+    NEResize,
+    NWResize,
+    SResize,
+    SEResize,
+    SWResize,
+    WResize,
+    EWResize,
+    NSResize,
+    NESWResize,
+    NWSEResize,
+    ColResize,
+    RowResize,
+    AllScroll,
+    ZoomIn,
+    ZoomOut,
+}
+
+impl fmt::Display for Cursor {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Cursor::Auto => f.write_str("auto"),
+            Cursor::Default => f.write_str("default"),
+            Cursor::None => f.write_str("none"),
+            Cursor::ContextMenu => f.write_str("context-menu"),
+            Cursor::Help => f.write_str("help"),
+            Cursor::Pointer => f.write_str("pointer"),
+            Cursor::Progress => f.write_str("progress"),
+            Cursor::Wait => f.write_str("wait"),
+            Cursor::Cell => f.write_str("cell"),
+            Cursor::Crosshair => f.write_str("crosshair"),
+            Cursor::Text => f.write_str("text"),
+            Cursor::VerticalText => f.write_str("vertical-text"),
+            Cursor::Alias => f.write_str("alias"),
+            Cursor::Copy => f.write_str("copy"),
+            Cursor::Move => f.write_str("move"),
+            Cursor::NoDrop => f.write_str("no-drop"),
+            Cursor::NotAllowed => f.write_str("not-allowed"),
+            Cursor::Grab => f.write_str("grab"),
+            Cursor::Grabbing => f.write_str("grabbing"),
+            Cursor::EResize => f.write_str("e-resize"),
+            Cursor::NResize => f.write_str("n-resize"),
+            Cursor::NEResize => f.write_str("ne-resize"),
+            Cursor::NWResize => f.write_str("nw-resize"),
+            Cursor::SResize => f.write_str("s-resize"),
+            Cursor::SEResize => f.write_str("se-resize"),
+            Cursor::SWResize => f.write_str("sw-resize"),
+            Cursor::WResize => f.write_str("w-resize"),
+            Cursor::EWResize => f.write_str("ew-resize"),
+            Cursor::NSResize => f.write_str("ns-resize"),
+            Cursor::NESWResize => f.write_str("nesw-resize"),
+            Cursor::NWSEResize => f.write_str("nwse-resize"),
+            Cursor::ColResize => f.write_str("col-resize"),
+            Cursor::RowResize => f.write_str("row-resize"),
+            Cursor::AllScroll => f.write_str("all-scroll"),
+            Cursor::ZoomIn => f.write_str("zoom-in"),
+            Cursor::ZoomOut => f.write_str("zoom-out"),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum Display {
+    Block,
+    Flex,
+    Inline,
+    // todo incomplete
+}
+
+impl fmt::Display for Display {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Display::Block => f.write_str("block"),
+            Display::Flex => f.write_str("flex"),
+            Display::Inline => f.write_str("inline"),
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum FlexBasis {
+    Width(Width21),
+    Content,
+}
+
+impl fmt::Display for FlexBasis {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            FlexBasis::Width(v) => fmt::Display::fmt(v, f),
+            FlexBasis::Content => f.write_str("content"),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum FlexDirection {
+    Row,
+    Column,
+}
+
+impl fmt::Display for FlexDirection {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            FlexDirection::Row => f.write_str("row"),
+            FlexDirection::Column => f.write_str("column"),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum FlexWrap {
+    Wrap,
+    Nowrap,
+}
+
+impl fmt::Display for FlexWrap {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            FlexWrap::Wrap => write!(f, "wrap"),
+            FlexWrap::Nowrap => write!(f, "nowrap"),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum Float {
+    None,
+    Left,
+    Right,
+    InlineStart,
+    InlineEnd,
+}
+
+impl fmt::Display for Float {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Float::None => f.write_str("inline-end"),
+            Float::Left => f.write_str("left"),
+            Float::Right => f.write_str("right"),
+            Float::InlineStart => f.write_str("inline-start"),
+            Float::InlineEnd => f.write_str("inline-end"),
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum Font {
+    // todo escape when `Display`ing
+    Named(String),
+    Serif,
+    SansSerif,
+    Cursive,
+    Fantasy,
+    Monospace,
+}
+
+impl fmt::Display for Font {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Font::Named(inner) => write!(f, "\"{}\"", inner),
+            Font::Serif => write!(f, "serif"),
+            Font::SansSerif => write!(f, "sans-serif"),
+            Font::Cursive => write!(f, "cursive"),
+            Font::Fantasy => write!(f, "fantasy"),
+            Font::Monospace => write!(f, "monospace"),
+        }
+    }
+}
+
+pub type FontFamily = NonemptyCommaList<Font>;
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum FontSize {
+    XXSmall,
+    XSmall,
+    Small,
+    Medium,
+    Large,
+    XLarge,
+    XXLarge,
+    XXXLarge,
+    Larger,
+    Smaller,
+    LengthPercentage(Calc),
+}
+
+impl fmt::Display for FontSize {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            FontSize::XXSmall => f.write_str("xx-small"),
+            FontSize::XSmall => f.write_str("x-small"),
+            FontSize::Small => f.write_str("small"),
+            FontSize::Medium => f.write_str("medium"),
+            FontSize::Large => f.write_str("large"),
+            FontSize::XLarge => f.write_str("x-large"),
+            FontSize::XXLarge => f.write_str("xx-large"),
+            FontSize::XXXLarge => f.write_str("xxx-large"),
+            FontSize::Larger => f.write_str("larger"),
+            FontSize::Smaller => f.write_str("smaller"),
+            FontSize::LengthPercentage(v) => fmt::Display::fmt(v, f),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum FontStyle {
+    Normal,
+    Italic,
+    Oblique,
+}
+
+impl fmt::Display for FontStyle {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            FontStyle::Normal => f.write_str("normal"),
+            FontStyle::Italic => f.write_str("italic"),
+            FontStyle::Oblique => f.write_str("oblique"),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum FontWeight {
+    Normal,
+    Bold,
+    Lighter,
+    Bolder,
+    /// Between 1 and 1000
+    Number(f64),
+}
+
+impl fmt::Display for FontWeight {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            FontWeight::Normal => f.write_str("normal"),
+            FontWeight::Bold => f.write_str("bold"),
+            FontWeight::Lighter => f.write_str("lighter"),
+            FontWeight::Bolder => f.write_str("bolder"),
+            FontWeight::Number(v) => fmt::Display::fmt(v, f),
+        }
+    }
+}
+
+/// https://www.w3.org/TR/css-flexbox-1/#propdef-justify-content
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum JustifyContent {
+    FlexStart,
+    Center,
+    FlexEnd,
+    SpaceBetween,
+    SpaceAround,
+}
+
+impl fmt::Display for JustifyContent {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            JustifyContent::FlexStart => write!(f, "flex-start"),
+            JustifyContent::Center => write!(f, "center"),
+            JustifyContent::FlexEnd => write!(f, "flex-end"),
+            JustifyContent::SpaceAround => write!(f, "space-around"),
+            JustifyContent::SpaceBetween => write!(f, "space-between"),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum Length {
+    Em(f64),
+    Ex(f64),
+    In(f64),
+    Cm(f64),
+    Mm(f64),
+    Pt(f64),
+    Pc(f64),
+    Px(f64),
+    Zero,
+}
+
+impl fmt::Display for Length {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Length::Em(val) => write!(f, "{}em", val),
+            Length::Ex(val) => write!(f, "{}ex", val),
+            Length::In(val) => write!(f, "{}in", val),
+            Length::Cm(val) => write!(f, "{}cm", val),
+            Length::Mm(val) => write!(f, "{}mm", val),
+            Length::Pt(val) => write!(f, "{}pt", val),
+            Length::Pc(val) => write!(f, "{}pc", val),
+            Length::Px(val) => write!(f, "{}px", val),
+            Length::Zero => write!(f, "0"),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum LengthPercentage {
+    Length(Length),
+    Percentage(Percentage),
+}
+
+impl fmt::Display for LengthPercentage {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            LengthPercentage::Length(v) => fmt::Display::fmt(v, f),
+            LengthPercentage::Percentage(v) => fmt::Display::fmt(v, f),
+        }
+    }
+}
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum LineStyle {
+    None,
+    Hidden,
+    Dotted,
+    Dashed,
+    Solid,
+    Double,
+    Groove,
+    Ridge,
+    Inset,
+    Outset,
+}
+
+impl fmt::Display for LineStyle {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            LineStyle::None => write!(f, "none"),
+            LineStyle::Hidden => write!(f, "hidden"),
+            LineStyle::Dotted => write!(f, "dotted"),
+            LineStyle::Dashed => write!(f, "dashed"),
+            LineStyle::Solid => write!(f, "solid"),
+            LineStyle::Double => write!(f, "double"),
+            LineStyle::Groove => write!(f, "groove"),
+            LineStyle::Ridge => write!(f, "ridge"),
+            LineStyle::Inset => write!(f, "inset"),
+            LineStyle::Outset => write!(f, "outset"),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum LineWidth {
+    Length(Length),
+    Thin,
+    Medium,
+    Thick,
+}
+
+impl fmt::Display for LineWidth {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            LineWidth::Length(v) => fmt::Display::fmt(v, f),
+            LineWidth::Thin => write!(f, "thin"),
+            LineWidth::Medium => write!(f, "medium"),
+            LineWidth::Thick => write!(f, "thick"),
+        }
+    }
+}
+
+// TODO this isn't the full spec for lineheight
+// (https://www.w3.org/TR/CSS2/visudet.html#propdef-line-height)
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct LineHeight(f64);
+
+impl fmt::Display for LineHeight {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{}", self.0)
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum ListStyleType {
+    Disc,
+    Circle,
+    Square,
+    Decimal,
+    DecimalLeadingZero,
+    LowerRoman,
+    UpperRoman,
+    LowerGreek,
+    UpperGreek,
+    LowerLatin,
+    UpperLatin,
+    Armenian,
+    Georgian,
+    LowerAlpha,
+    UpperAlpha,
+    None,
+}
+
+impl fmt::Display for ListStyleType {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            ListStyleType::Disc => write!(f, "disc"),
+            ListStyleType::Circle => write!(f, "circle"),
+            ListStyleType::Square => write!(f, "square"),
+            ListStyleType::Decimal => write!(f, "decimal"),
+            ListStyleType::DecimalLeadingZero => write!(f, "decimal-leading-zero"),
+            ListStyleType::LowerRoman => write!(f, "lower-roman"),
+            ListStyleType::UpperRoman => write!(f, "upper-roman"),
+            ListStyleType::LowerGreek => write!(f, "lower-greek"),
+            ListStyleType::UpperGreek => write!(f, "upper-greek"),
+            ListStyleType::LowerLatin => write!(f, "lower-latin"),
+            ListStyleType::UpperLatin => write!(f, "upper-latin"),
+            ListStyleType::Armenian => write!(f, "armenian"),
+            ListStyleType::Georgian => write!(f, "georgian"),
+            ListStyleType::LowerAlpha => write!(f, "lower-alpha"),
+            ListStyleType::UpperAlpha => write!(f, "upper-alpha"),
+            ListStyleType::None => write!(f, "none"),
+        }
+    }
+}
+
+pub type Margin = Rect<MarginWidth>;
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum AutoLengthPercentage {
+    LengthPercentage(Calc),
+    Auto,
+}
+
+impl fmt::Display for AutoLengthPercentage {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            AutoLengthPercentage::LengthPercentage(v) => fmt::Display::fmt(v, f),
+            AutoLengthPercentage::Auto => write!(f, "auto"),
+        }
+    }
+}
+
+pub type MarginWidth = AutoLengthPercentage;
+
+/// for max-width and max-height
+#[derive(Debug, Clone, PartialEq)]
+pub enum MaxWidthHeight {
+    None,
+    LengthPercentage(Calc),
+    MinContent,
+    MaxContent,
+    FitContent(Calc),
+}
+
+impl fmt::Display for MaxWidthHeight {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            MaxWidthHeight::None => write!(f, "none"),
+            MaxWidthHeight::LengthPercentage(v) => write!(f, "{}", v),
+            MaxWidthHeight::MinContent => write!(f, "min-content"),
+            MaxWidthHeight::MaxContent => write!(f, "max-content"),
+            MaxWidthHeight::FitContent(v) => write!(f, "fit-content({})", v),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum ObjectFit {
+    Fill,
+    None,
+    Contain { scale_down: bool },
+    Cover { scale_down: bool },
+}
+
+impl fmt::Display for ObjectFit {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            ObjectFit::Fill => write!(f, "fill"),
+            ObjectFit::None => write!(f, "none"),
+            ObjectFit::Contain { scale_down } => {
+                if *scale_down {
+                    write!(f, "contain scale-down")
+                } else {
+                    write!(f, "contain")
+                }
+            }
+            ObjectFit::Cover { scale_down } => {
+                if *scale_down {
+                    write!(f, "cover scale-down")
+                } else {
+                    write!(f, "cover")
+                }
+            }
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum Overflow {
+    Both(OverflowXY),
+    XY(OverflowXY, OverflowXY),
+}
+
+impl fmt::Display for Overflow {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Overflow::Both(v) => write!(f, "{}", v),
+            Overflow::XY(x, y) => write!(f, "{} {}", x, y),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum OverflowXY {
+    Visible,
+    Hidden,
+    Clip,
+    Scroll,
+    Auto,
+}
+
+impl fmt::Display for OverflowXY {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            OverflowXY::Visible => write!(f, "visible"),
+            OverflowXY::Hidden => write!(f, "hidden"),
+            OverflowXY::Clip => write!(f, "clip"),
+            OverflowXY::Scroll => write!(f, "scroll"),
+            OverflowXY::Auto => write!(f, "auto"),
+        }
+    }
+}
+
+pub type Padding = Rect<Calc>;
+
+/// for e.g. `padding-top`
+pub type PaddingWidth = Calc;
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Percentage(pub f64);
+
+impl fmt::Display for Percentage {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{}%", self.0)
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum Position {
+    Static,
+    Relative,
+    Absolute,
+    Fixed,
+}
+
+impl fmt::Display for Position {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Position::Static => write!(f, "static"),
+            Position::Relative => write!(f, "relative"),
+            Position::Absolute => write!(f, "absolute"),
+            Position::Fixed => write!(f, "fixed"),
+        }
+    }
+}
+
+/// For parsing things in groups of 1, 2, 3 or 4 for specifying the sides of a rectangle.
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum Rect<T> {
+    All(T),
+    VerticalHorizontal(T, T),
+    TopHorizontalBottom(T, T, T),
+    TopRightBottomLeft(T, T, T, T),
+}
+
+impl<T> fmt::Display for Rect<T>
+where
+    T: fmt::Display,
+{
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Rect::All(a) => write!(f, "{}", a),
+            Rect::VerticalHorizontal(v, h) => write!(f, "{} {}", v, h),
+            Rect::TopHorizontalBottom(t, h, b) => write!(f, "{} {} {}", t, h, b),
+            Rect::TopRightBottomLeft(t, r, b, l) => write!(f, "{} {} {} {}", t, r, b, l),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum Resize {
+    None,
+    Both,
+    Horizontal,
+    Vertical,
+}
+
+impl fmt::Display for Resize {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Resize::None => write!(f, "none"),
+            Resize::Both => write!(f, "both"),
+            Resize::Horizontal => write!(f, "horizontal"),
+            Resize::Vertical => write!(f, "vertical"),
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct Shadow {
+    pub color: Option<Color>,
+    pub length: ShadowLength,
+    pub inset: bool,
+}
+
+impl fmt::Display for Shadow {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        // we do it in this order because it makes spacing easier.
+        write!(f, "{}", self.length)?;
+        if let Some(color) = self.color {
+            write!(f, " {}", color)?;
+        }
+        if self.inset {
+            write!(f, " inset")?;
+        }
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum ShadowLength {
+    Offsets {
+        horizontal: Length,
+        vertical: Length,
+    },
+    OffsetsBlur {
+        horizontal: Length,
+        vertical: Length,
+        blur: Length,
+    },
+    OffsetsBlurSpread {
+        horizontal: Length,
+        vertical: Length,
+        blur: Length,
+        spread: Length,
+    },
+}
+
+impl fmt::Display for ShadowLength {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            ShadowLength::Offsets {
+                horizontal,
+                vertical,
+            } => write!(f, "{} {}", horizontal, vertical),
+            ShadowLength::OffsetsBlur {
+                horizontal,
+                vertical,
+                blur,
+            } => write!(f, "{} {} {}", horizontal, vertical, blur),
+            ShadowLength::OffsetsBlurSpread {
+                horizontal,
+                vertical,
+                blur,
+                spread,
+            } => write!(f, "{} {} {} {}", horizontal, vertical, blur, spread),
+        }
+    }
+}
+
+#[test]
+fn test_shadow_length() {
+    for (input, output) in vec![
+        (
+            "0 10px",
+            ShadowLength::Offsets {
+                horizontal: Length::Zero,
+                vertical: Length::Px(10.0),
+            },
+        ),
+        (
+            "0 10px -10px",
+            ShadowLength::OffsetsBlur {
+                horizontal: Length::Zero,
+                vertical: Length::Px(10.0),
+                blur: Length::Px(-10.0),
+            },
+        ),
+    ] {
+        assert_eq!(syn::parse_str::<ShadowLength>(input).unwrap(), output)
+    }
+}
+
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub enum TextAlign {
+    Left,
+    Right,
+    Center,
+    Justify,
+}
+
+impl fmt::Display for TextAlign {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            TextAlign::Left => write!(f, "left"),
+            TextAlign::Right => write!(f, "right"),
+            TextAlign::Center => write!(f, "center"),
+            TextAlign::Justify => write!(f, "justify"),
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct Url {
+    // todo modifiers
+    url: String,
+}
+
+impl fmt::Display for Url {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "url(\"{}\")", self.url)
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum WhiteSpace {
+    Normal,
+    Pre,
+    Nowrap,
+    PreWrap,
+    PreLine,
+}
+
+impl fmt::Display for WhiteSpace {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            WhiteSpace::Normal => write!(f, "normal"),
+            WhiteSpace::Pre => write!(f, "pre"),
+            WhiteSpace::Nowrap => write!(f, "nowrap"),
+            WhiteSpace::PreWrap => write!(f, "pre-wrap"),
+            WhiteSpace::PreLine => write!(f, "pre-line"),
+        }
+    }
+}
+
+/// values of `width` and `height`, `min-width`, `min-height`.
+#[derive(Debug, Clone, PartialEq)]
+pub enum WidthHeight {
+    Auto,
+    LengthPercentage(Calc),
+    MinContent,
+    MaxContent,
+    FitContent(Calc),
+}
+
+impl fmt::Display for WidthHeight {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            WidthHeight::Auto => write!(f, "auto"),
+            WidthHeight::LengthPercentage(v) => write!(f, "{}", v),
+            WidthHeight::MinContent => write!(f, "min-content"),
+            WidthHeight::MaxContent => write!(f, "max-content"),
+            WidthHeight::FitContent(v) => write!(f, "fit-content({})", v),
+        }
+    }
+}
+
+/// CSS2.1 width, for use with flexbox.
+#[derive(Debug, Clone, PartialEq)]
+pub enum Width21 {
+    Auto,
+    LengthPercentage(Calc),
+}
+
+impl fmt::Display for Width21 {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Width21::Auto => write!(f, "auto"),
+            Width21::LengthPercentage(v) => fmt::Display::fmt(v, f),
+        }
+    }
+}
+
+/// A generic container for a non-empty comma-separated list of values
+#[derive(Debug, Clone, PartialEq)]
+pub struct NonemptyCommaList<T> {
+    first: T,
+    rest: Vec<T>,
+}
+
+impl<T> fmt::Display for NonemptyCommaList<T>
+where
+    T: fmt::Display,
+{
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{}", self.first)?;
+        for t in &self.rest {
+            write!(f, ",{}", t)?;
+        }
+        Ok(())
+    }
+}
+
+/// Matches one or two variables.
+#[derive(Debug, Clone, PartialEq)]
+pub enum SingleOrDouble<T> {
+    Single(T),
+    Double { horiz: T, vert: T },
+}
+
+impl<T> fmt::Display for SingleOrDouble<T>
+where
+    T: fmt::Display,
+{
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            SingleOrDouble::Single(t) => t.fmt(f),
+            SingleOrDouble::Double { vert, horiz } => write!(f, "{} {}", vert, horiz),
+        }
+    }
+}

+ 502 - 0
packages/core-macro/src/styles/string/lexer.rs

@@ -0,0 +1,502 @@
+//! Parse the various css types from strings directly (avoid pulling in syn if working at runtime)
+//!
+//! Differences to spec:
+//!  - Exponential floats are not supported for now.
+use std::{char, fmt, iter};
+
+const REPLACEMENT_CHAR: char = '�';
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+#[non_exhaustive] // Don't allow user to create
+pub struct Span {
+    /// Inclusive
+    start: usize,
+    /// Exclusive
+    end: usize,
+}
+
+impl Span {
+    fn new(start: usize, end: usize) -> Self {
+        assert!(end > start, "end must be greater than start");
+        Span { start, end }
+    }
+
+    pub fn len(&self) -> usize {
+        self.end - self.start
+    }
+}
+
+#[derive(Debug)]
+pub struct InvalidChar {
+    ch: char,
+    pos: usize,
+}
+
+impl fmt::Display for InvalidChar {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(
+            f,
+            "invalid character `{}` found at position {}",
+            self.ch.escape_debug(),
+            self.pos
+        )
+    }
+}
+
+#[derive(Debug)]
+pub struct Lexer<'src> {
+    src: &'src str,
+    cursor: usize,
+}
+
+impl<'src> Lexer<'src> {
+    pub fn new(src: &'src str) -> Result<Lexer<'src>, InvalidChar> {
+        // Check that the user has already replaced characters as specified at
+        // https://www.w3.org/TR/css-syntax-3/#input-preprocessing
+        for (pos, ch) in src.char_indices() {
+            if ch == '\r' || ch == '\u{d}' || ch == '\0' {
+                return Err(InvalidChar { ch, pos });
+            }
+        }
+        Ok(Lexer { src, cursor: 0 })
+    }
+
+    fn len(&self) -> usize {
+        self.src.len()
+    }
+
+    fn remaining(&self) -> usize {
+        self.src.len() - self.cursor
+    }
+
+    pub fn next_token(&mut self) -> Option<Token> {
+        match self.peek() {
+            Some(token) => {
+                self.consume(&token);
+                Some(token)
+            }
+            None => None,
+        }
+    }
+
+    pub fn peek(&self) -> Option<Token> {
+        // https://www.w3.org/TR/css-syntax-3/#tokenizer-definitions
+        if let Some(comment) = self.comment() {
+            return Some(comment);
+        }
+        if let Some(tok) = self.whitespace() {
+            return Some(tok);
+        }
+        if let Some(tok) = self.string() {
+            return Some(tok);
+        }
+        match self.chars().next() {
+            Some(other) => Some(Token::new(
+                TokenKind::Error,
+                Span::new(self.cursor, self.cursor + other.len_utf8()),
+            )),
+            None => None,
+        }
+    }
+
+    pub fn peek_n(&self, n: usize) -> Option<Token> {
+        todo!()
+    }
+
+    pub fn is_empty(&self) -> bool {
+        todo!() //self.peek().is_none()
+    }
+
+    pub fn resolve_span(&self, span: Span) -> &'src str {
+        if span.end > self.len() {
+            panic!("End of requested span is past the end of the source");
+        }
+        &self.src[span.start..span.end]
+    }
+
+    /// Create another independent lexer at the given start point
+    fn fork(&self) -> Lexer {
+        Lexer {
+            src: self.src,
+            cursor: self.cursor,
+        }
+    }
+
+    pub fn consume(&mut self, tok: &Token) {
+        assert!(
+            tok.len() <= self.remaining(),
+            "trying to consume a token that would be bigger \
+            than all remaining text"
+        );
+        self.cursor += tok.len();
+    }
+
+    /// Resolve a position from cursor to position from start of src
+    fn resolve_pos(&self, pos: usize) -> usize {
+        self.cursor + pos
+    }
+
+    /// Create a span from the current position with the given length
+    fn span(&self, len: usize) -> Span {
+        debug_assert!(self.cursor + len <= self.len());
+        Span::new(self.cursor, self.cursor + len)
+    }
+
+    /// Create a span from the current position to the end
+    fn span_to_end(&self) -> Span {
+        Span::new(self.cursor, self.len())
+    }
+
+    /// Iterate over the remaining chars of the input
+    fn chars(&self) -> std::str::Chars {
+        self.src[self.cursor..].chars()
+    }
+
+    /// Iterate over the remaining chars of the input
+    fn char_indices(&self) -> std::str::CharIndices {
+        self.src[self.cursor..].char_indices()
+    }
+
+    /// Parse a comment
+    fn comment(&self) -> Option<Token> {
+        let mut ch_iter = self.char_indices().peekable();
+        if let Some((_, '/')) = ch_iter.next() {
+            if let Some((_, '*')) = ch_iter.next() {
+                loop {
+                    match ch_iter.next() {
+                        Some((_, '*')) => {
+                            if let Some((idx, '/')) = ch_iter.peek() {
+                                return Some(Token {
+                                    kind: TokenKind::Comment,
+                                    span: self.span(*idx + '/'.len_utf8()),
+                                });
+                            }
+                        }
+                        None => {
+                            return Some(Token::new(
+                                TokenKind::UnclosedComment,
+                                self.span_to_end(),
+                            ));
+                        }
+                        _ => (),
+                    }
+                }
+            }
+        }
+        None
+    }
+
+    /// Parse whitespace
+    fn whitespace(&self) -> Option<Token> {
+        let mut ch_iter = self.chars();
+        let mut len = match ch_iter.next() {
+            Some(ch) if ch.is_ascii_whitespace() => ch.len_utf8(),
+            _ => return None,
+        };
+        loop {
+            match ch_iter.next() {
+                Some(ch) if ch.is_ascii_whitespace() => len += ch.len_utf8(),
+                _ => break,
+            }
+        }
+        Some(Token {
+            kind: TokenKind::Whitespace,
+            span: self.span(len),
+        })
+    }
+
+    /// Parse either a single or double quoted string
+    fn string(&self) -> Option<Token> {
+        let mut ch_iter = self.char_indices().fuse().peekable();
+        let delim = match ch_iter.next() {
+            Some((_, '"')) => '"',
+            Some((_, '\'')) => '\'',
+            _ => return None,
+        };
+        let mut decoded_string = String::new();
+        loop {
+            match ch_iter.next() {
+                Some((end, ch)) if ch == delim => {
+                    return Some(Token {
+                        kind: TokenKind::String(decoded_string),
+                        span: self.span(end + 1), // '"'.len_utf8() == 1
+                    });
+                }
+                Some((end, '\n')) => {
+                    return Some(Token {
+                        kind: TokenKind::BadString(decoded_string),
+                        span: self.span(end + 1), // '\n'.len_utf8() == 1
+                    });
+                }
+                Some((_, '\\')) => match ch_iter.peek() {
+                    Some((_, ch)) => {
+                        if *ch == '\n' {
+                            // do nothing - skip the backslash and newline.
+                            ch_iter.next().unwrap();
+                        } else if let Some(decoded_ch) = unescape(&mut ch_iter) {
+                            decoded_string.push(decoded_ch);
+                        } else {
+                            decoded_string.push(ch_iter.next().unwrap().1);
+                        }
+                    }
+                    None => {
+                        // The spec says not to add the last '\'.
+                        // a bad string will be returned on next pass
+                        ch_iter.next().unwrap();
+                    }
+                },
+                Some((_, ch)) => decoded_string.push(ch),
+                None => {
+                    return Some(Token {
+                        kind: TokenKind::BadString(decoded_string),
+                        span: self.span_to_end(),
+                    })
+                }
+            }
+        }
+    }
+
+    /*
+    fn hash(&self) -> Option<Token> {
+        let mut iter = self.char_indices();
+        match iter.next() {
+            Some((_, '#')) => (),
+            None => return None,
+        };
+        match iter.next() {
+            Some((_, '\\')) => {}
+            _ => Some(Token {
+                kind: TokenKind::Delim('#'),
+                span: self.span(1),
+            }),
+        }
+    }
+    */
+}
+
+impl<'src> Iterator for Lexer<'src> {
+    type Item = Token;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        self.next_token()
+    }
+}
+
+#[derive(Debug, PartialEq)]
+#[non_exhaustive]
+pub struct Token {
+    pub kind: TokenKind,
+    pub span: Span,
+}
+
+impl Token {
+    fn new(kind: TokenKind, span: Span) -> Self {
+        Token { kind, span }
+    }
+
+    pub fn len(&self) -> usize {
+        self.span.len()
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub enum TokenKind {
+    Ident,
+    Function,
+    At,
+    Hash,
+    String(String),
+    BadString(String),
+    Url,
+    BadUrl,
+    Delim(char),
+    Number,
+    Percentage,
+    Dimension,
+    Whitespace,
+    /// <!--
+    CDO,
+    /// -->
+    CDC,
+    /// :
+    Colon,
+    /// ;
+    Semicolon,
+    /// ,
+    Comma,
+    /// [
+    LBracket,
+    /// ]
+    RBracket,
+    /// (
+    LParen,
+    /// )
+    RParen,
+    /// {
+    LBrace,
+    /// }
+    RBrace,
+    Comment,
+    UnclosedComment,
+    /// Could not parse the next token
+    Error,
+}
+
+// Helpers
+
+/// Hex to char (up to 6 characters, e.g. "ffffff").
+///
+/// For example `"5c" => '\'`. Returns None if first char is not hex.  Consumes the hex values.
+fn unescape(input: &mut iter::Peekable<impl Iterator<Item = (usize, char)>>) -> Option<char> {
+    fn hex_acc(acc: &mut u32, next: char) {
+        debug_assert!(*acc & 0xf0000000 == 0); // make sure we don't overflow
+        (*acc) = (*acc << 4) + next.to_digit(16).unwrap()
+    }
+
+    let (_, ch) = match input.peek() {
+        Some((idx, ch)) if ch.is_ascii_hexdigit() => input.next().unwrap(),
+        _ => return None,
+    };
+
+    let mut acc = 0;
+    let mut count = 0;
+    hex_acc(&mut acc, ch);
+
+    // Here we use that the length of all valid hexdigits in utf8 is 1.
+    while count < 5
+        && input
+            .peek()
+            .map(|(_, ch)| ch.is_ascii_hexdigit())
+            .unwrap_or(false)
+    {
+        let ch = input.next().unwrap().1;
+        hex_acc(&mut acc, ch);
+        count += 1;
+    }
+
+    // consume a whitespace char if it's there
+    if input
+        .peek()
+        .map(|(_, ch)| ch.is_ascii_whitespace())
+        .unwrap_or(false)
+    {
+        input.next().unwrap();
+    }
+
+    // maybe we could just directly use `char::from_u32(acc).unwrap_or(REPLACEMENT_CHAR)`
+    // null, surrogate, or too big
+    Some(
+        if acc == 0 || (acc >= 0xd800 && acc < 0xe000) || acc >= 0x110000 {
+            REPLACEMENT_CHAR
+        } else {
+            char::from_u32(acc).unwrap() // there should be no other invalid chars.
+        },
+    )
+}
+
+#[cfg(test)]
+mod test {
+    use super::{Lexer, Span, Token, TokenKind};
+
+    #[test]
+    fn comment() {
+        println!();
+        let mut input = Lexer::new("/* a valid comment */").unwrap();
+        match input.next_token() {
+            Some(Token {
+                kind: TokenKind::Comment,
+                span,
+            }) => {
+                assert_eq!(
+                    input.resolve_span(span),
+                    "/* a valid comment */".to_string()
+                );
+                assert_eq!(span.len(), 21);
+            }
+            _ => panic!("not a comment"),
+        };
+
+        let mut input = Lexer::new("/* a comment").unwrap();
+        match input.next_token() {
+            Some(Token {
+                kind: TokenKind::UnclosedComment,
+                span,
+            }) => {
+                assert_eq!(input.resolve_span(span), "/* a comment".to_string());
+                assert_eq!(span.len(), 12);
+            }
+            _ => panic!("not a comment"),
+        };
+
+        let mut input = Lexer::new("/!* not a comment").unwrap();
+        match input.next_token() {
+            Some(Token {
+                kind: TokenKind::Error,
+                span,
+            }) => {}
+            _ => panic!("not a comment"),
+        };
+    }
+
+    #[test]
+    fn string() {
+        println!("h");
+        let mut input = Lexer::new("\" a vali\\64\\e9 \\\n string \"").unwrap();
+        match input.next_token() {
+            Some(Token {
+                kind: TokenKind::String(s),
+                span,
+            }) => {
+                assert_eq!(s, " a validé string ".to_string());
+                assert_eq!(span.len(), 26);
+            }
+            _ => panic!("not a string"),
+        };
+
+        let mut input = Lexer::new("' a valid string '").unwrap();
+        match input.next_token() {
+            Some(Token {
+                kind: TokenKind::String(s),
+                span,
+            }) => {
+                assert_eq!(s, " a valid string ".to_string());
+                assert_eq!(span.len(), 18);
+            }
+            _ => panic!("not a string"),
+        };
+
+        let mut input = Lexer::new("\" a string").unwrap();
+        match input.next_token() {
+            Some(Token {
+                kind: TokenKind::BadString(s),
+                span,
+            }) => {
+                assert_eq!(s, " a string".to_string());
+                assert_eq!(span.len(), 10);
+            }
+            _ => panic!("not a string"),
+        };
+    }
+
+    #[test]
+    fn whitespace() {
+        println!();
+        let mut input = Lexer::new("\n\t ").unwrap();
+        match input.next_token() {
+            Some(Token {
+                kind: TokenKind::Whitespace,
+                span,
+            }) => {
+                assert_eq!(input.resolve_span(span), "\n\t ".to_string());
+                assert_eq!(span.len(), 3);
+            }
+            _ => panic!("not a string"),
+        };
+    }
+
+    #[test]
+    fn escape() {
+        let mut iter = "e9".char_indices().peekable();
+        assert_eq!(super::unescape(&mut iter), Some('é'));
+    }
+}

+ 1 - 0
packages/core-macro/src/styles/string/mod.rs

@@ -0,0 +1 @@
+pub mod lexer;

+ 2411 - 0
packages/core-macro/src/styles/syn_parse.rs

@@ -0,0 +1,2411 @@
+//! Implementation of `syn::parse::Parse` for styles, and associated helper data/functions.
+// TODO make all parsers use HyphenWord where appropriate.
+// TODO make all error messages nice
+// TODO 100% test coverage
+// TODO see if I can get https://github.com/rust-lang/rust/issues/67544 accepted. then change "em" to
+// em and "ex" to ex.
+// TODO Split out extra "Dynamic" layer for each type for use in proc macro (so we can have `{ <arbitary
+// rust code> }`)
+use crate::*;
+use proc_macro2::Span;
+use std::{
+    cell::RefCell,
+    collections::BTreeSet,
+    fmt::{self, Write},
+    ops::RangeBounds,
+    str,
+};
+use syn::{
+    ext::IdentExt,
+    parse::{discouraged::Speculative, Parse, ParseStream},
+    punctuated::Punctuated,
+    spanned::Spanned,
+    Ident, Token,
+};
+
+use super::{DynamicStyle, DynamicStyles, Styles};
+
+impl Parse for DynamicStyles {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let punc = s.parse_terminated::<_, Token![;]>(<DynamicStyle as Parse>::parse)?;
+        Ok(DynamicStyles::from(punc.into_iter().collect::<Vec<_>>()))
+    }
+}
+
+impl Parse for Styles {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let punc = s.parse_terminated::<_, Token![;]>(<Style as Parse>::parse)?;
+        Ok(Styles::from(punc.into_iter().collect::<Vec<_>>()))
+    }
+}
+
+impl Parse for DynamicStyle {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        // Pass through brackets
+        if s.peek(syn::token::Brace) {
+            Ok(DynamicStyle::Dynamic(s.parse()?))
+        } else {
+            Ok(DynamicStyle::Literal(s.parse()?))
+        }
+    }
+}
+
+impl Parse for Style {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        if s.peek(syn::LitStr) {
+            let unchecked: syn::LitStr = s.parse()?;
+            return Ok(Style::Unchecked(unchecked.value()));
+        }
+
+        let name: HyphenWord = s.parse()?;
+        if name.try_match("dummy") {
+            return Ok(Style::Dummy);
+        }
+
+        s.parse::<Token![:]>()?;
+
+        let output = if name.try_match("align-content") {
+            Style::AlignContent(s.parse()?)
+        } else if name.try_match("align-items") {
+            Style::AlignItems(s.parse()?)
+        } else if name.try_match("align-self") {
+            Style::AlignSelf(s.parse()?)
+        // all
+        // background
+        } else if name.try_match("background-attachment") {
+            Style::BackgroundAttachment(s.parse()?)
+        } else if name.try_match("background-blend-mode") {
+            Style::BackgroundBlendMode(s.parse()?)
+        } else if name.try_match("background-clip") {
+            Style::BackgroundClip(s.parse()?)
+        } else if name.try_match("background-color") {
+            Style::BackgroundColor(s.parse()?)
+        } else if name.try_match("background-image") {
+            Style::BackgroundImage(s.parse()?)
+        } else if name.try_match("background-origin") {
+            Style::BackgroundOrigin(s.parse()?)
+        } else if name.try_match("background-position") {
+            Style::BackgroundPosition(s.parse()?)
+        } else if name.try_match("background-repeat") {
+            Style::BackgroundRepeat(s.parse()?)
+        } else if name.try_match("background-size") {
+            Style::BackgroundSize(s.parse()?)
+        } else if name.try_match("border") {
+            Style::Border(s.parse()?)
+        } else if name.try_match("border-bottom") {
+            Style::BorderBottom(s.parse()?)
+        } else if name.try_match("border-bottom-color") {
+            Style::BorderBottomColor(s.parse()?)
+        } else if name.try_match("border-bottom-left-radius") {
+            Style::BorderBottomLeftRadius(s.parse()?)
+        } else if name.try_match("border-bottom-right-radius") {
+            Style::BorderBottomRightRadius(s.parse()?)
+        } else if name.try_match("border-bottom-style") {
+            Style::BorderBottomStyle(s.parse()?)
+        } else if name.try_match("border-bottom-width") {
+            Style::BorderBottomWidth(s.parse()?)
+        } else if name.try_match("border-collapse") {
+            Style::BorderCollapse(s.parse()?)
+        } else if name.try_match("border-color") {
+            Style::BorderColor(s.parse()?)
+        // border-image
+        // border-image-outset
+        // border-image-repeat
+        // border-image-slice
+        // border-image-source
+        // border-image-width
+        } else if name.try_match("border-left") {
+            Style::BorderLeft(s.parse()?)
+        } else if name.try_match("border-left-color") {
+            Style::BorderLeftColor(s.parse()?)
+        } else if name.try_match("border-left-style") {
+            Style::BorderLeftStyle(s.parse()?)
+        } else if name.try_match("border-left-width") {
+            Style::BorderLeftWidth(s.parse()?)
+        } else if name.try_match("border-radius") {
+            Style::BorderRadius(s.parse()?)
+        } else if name.try_match("border-right") {
+            Style::BorderRight(s.parse()?)
+        } else if name.try_match("border-right-color") {
+            Style::BorderRightColor(s.parse()?)
+        } else if name.try_match("border-right-style") {
+            Style::BorderRightStyle(s.parse()?)
+        } else if name.try_match("border-right-width") {
+            Style::BorderRightWidth(s.parse()?)
+        // border-spacing
+        } else if name.try_match("border-style") {
+            Style::BorderStyle(s.parse()?)
+        } else if name.try_match("border-top") {
+            Style::BorderTop(s.parse()?)
+        } else if name.try_match("border-top-color") {
+            Style::BorderTopColor(s.parse()?)
+        } else if name.try_match("border-top-left-radius") {
+            Style::BorderTopLeftRadius(s.parse()?)
+        } else if name.try_match("border-top-right-radius") {
+            Style::BorderTopRightRadius(s.parse()?)
+        } else if name.try_match("border-top-style") {
+            Style::BorderTopStyle(s.parse()?)
+        } else if name.try_match("border-top-width") {
+            Style::BorderTopWidth(s.parse()?)
+        } else if name.try_match("border-width") {
+            Style::BorderWidth(s.parse()?)
+        } else if name.try_match("bottom") {
+            Style::Bottom(s.parse()?)
+        // box-decoration-break
+        } else if name.try_match("box-shadow") {
+            Style::BoxShadow(s.parse()?)
+        } else if name.try_match("box-sizing") {
+            Style::BoxSizing(s.parse()?)
+        // break-after
+        // break-before
+        // break-inside
+        // caption-side
+        // caret-color
+        } else if name.try_match("clear") {
+            Style::Clear(s.parse()?)
+        // clip
+        // clip-path
+        // clip-rule
+        } else if name.try_match("column-count") {
+            Style::ColumnCount(s.parse()?)
+        } else if name.try_match("color") {
+            Style::Color(s.parse()?)
+        // contain
+        // content
+        // counter-increment
+        // counter-reset
+        // cue
+        // cue-after
+        // cue-before
+        } else if name.try_match("cursor") {
+            Style::Cursor(s.parse()?)
+        // direction
+        } else if name.try_match("display") {
+            Style::Display(s.parse()?)
+        // elevation
+        // empty-cells
+        // flex
+        } else if name.try_match("flex-basis") {
+            Style::FlexBasis(s.parse()?)
+        } else if name.try_match("flex-direction") {
+            Style::FlexDirection(s.parse()?)
+        // flex-flow
+        } else if name.try_match("flex-grow") {
+            let number: Number = s.parse()?;
+            if !number.suffix.is_empty() {
+                return Err(syn::Error::new(number.span, "expected number"));
+            }
+            Style::FlexGrow(number.value)
+        } else if name.try_match("flex-shrink") {
+            let number: Number = s.parse()?;
+            if !number.suffix.is_empty() {
+                return Err(syn::Error::new(number.span, "expected number"));
+            }
+            Style::FlexShrink(number.value)
+        } else if name.try_match("flex-wrap") {
+            Style::FlexWrap(s.parse()?)
+        } else if name.try_match("float") {
+            Style::Float(s.parse()?)
+        // font
+        } else if name.try_match("font-family") {
+            Style::FontFamily(s.parse()?)
+        // font-feature-settings
+        // font-kerning
+        } else if name.try_match("font-size") {
+            Style::FontSize(s.parse()?)
+        // font-size-adjust
+        // font-stretch
+        } else if name.try_match("font-style") {
+            Style::FontStyle(s.parse()?)
+        // font-synthesis
+        // font-variant
+        // font-variant-caps
+        // font-variant-east-asian
+        // font-variant-ligatures
+        // font-variant-numeric
+        // font-variant-position
+        } else if name.try_match("font-weight") {
+            Style::FontWeight(s.parse()?)
+        // glyph-orientation-vertical
+        // grid
+        // grid-area
+        // grid-auto-columns
+        // grid-auto-flow
+        // grid-auto-rows
+        // grid-column
+        // grid-column-end
+        // grid-column-start
+        // grid-row
+        // grid-row-end
+        // grid-row-start
+        // grid-template
+        // grid-template-areas
+        // grid-template-columns
+        // grid-template-rows
+        } else if name.try_match("height") {
+            Style::Height(s.parse()?)
+        // image-orientation
+        // image-rendering
+        // isolation
+        } else if name.try_match("justify-content") {
+            Style::JustifyContent(s.parse()?)
+        } else if name.try_match("left") {
+            Style::Left(s.parse()?)
+        // letter-spacing
+        } else if name.try_match("line-height") {
+            Style::LineHeight(s.parse()?)
+        // list-style
+        // list-style-image
+        // list-style-position
+        } else if name.try_match("list-style-type") {
+            Style::ListStyleType(s.parse()?)
+        } else if name.try_match("margin") {
+            Style::Margin(s.parse()?)
+        } else if name.try_match("margin-bottom") {
+            Style::MarginBottom(s.parse()?)
+        } else if name.try_match("margin-left") {
+            Style::MarginLeft(s.parse()?)
+        } else if name.try_match("margin-right") {
+            Style::MarginRight(s.parse()?)
+        } else if name.try_match("margin-top") {
+            Style::MarginTop(s.parse()?)
+        // mask
+        // mask-border
+        // mask-border-mode
+        // mask-border-outset
+        // mask-border-repeat
+        // mask-border-slice
+        // mask-border-source
+        // mask-border-width
+        // mask-clip
+        // mask-composite
+        // mask-image
+        // mask-mode
+        // mask-origin
+        // mask-position
+        // mask-repeat
+        // mask-size
+        // mask-type
+        } else if name.try_match("max-height") {
+            Style::MaxHeight(s.parse()?)
+        } else if name.try_match("max-width") {
+            Style::MaxWidth(s.parse()?)
+        } else if name.try_match("min-height") {
+            Style::MinHeight(s.parse()?)
+        } else if name.try_match("min-width") {
+            Style::MinWidth(s.parse()?)
+        // mix-blend-mode
+        } else if name.try_match("object-fit") {
+            Style::ObjectFit(s.parse()?)
+        // object-position
+        // opacity
+        // order
+        // orphans
+        // outline
+        // outline-color
+        // outline-offset
+        // outline-style
+        // outline-width
+        } else if name.try_match("overflow") {
+            Style::Overflow(s.parse()?)
+        } else if name.try_match("overflow-x") {
+            Style::OverflowX(s.parse()?)
+        } else if name.try_match("overflow-y") {
+            Style::OverflowY(s.parse()?)
+        } else if name.try_match("padding") {
+            Style::Padding(s.parse()?)
+        } else if name.try_match("padding-bottom") {
+            Style::PaddingBottom(s.parse()?)
+        } else if name.try_match("padding-left") {
+            Style::PaddingLeft(s.parse()?)
+        } else if name.try_match("padding-right") {
+            Style::PaddingRight(s.parse()?)
+        } else if name.try_match("padding-top") {
+            Style::PaddingTop(s.parse()?)
+        // page-break-after
+        // page-break-before
+        // page-break-inside
+        // pause
+        // pause-after
+        // pause-before
+        // pitch
+        // pitch-range
+        // play-during
+        } else if name.try_match("position") {
+            Style::Position(s.parse()?)
+        // quotes
+        } else if name.try_match("resize") {
+            Style::Resize(s.parse()?)
+        // richness
+        } else if name.try_match("right") {
+            Style::Right(s.parse()?)
+        // scroll-margin
+        // scroll-margin-block
+        // scroll-margin-block-end
+        // scroll-margin-block-start
+        // scroll-margin-bottom
+        // scroll-margin-inline
+        // scroll-margin-inline-end
+        // scroll-margin-inline-start
+        // scroll-margin-left
+        // scroll-margin-right
+        // scroll-margin-top
+        // scroll-padding
+        // scroll-padding-block
+        // scroll-padding-block-end
+        // scroll-padding-block-start
+        // scroll-padding-bottom
+        // scroll-padding-inline
+        // scroll-padding-inline-end
+        // scroll-padding-inline-start
+        // scroll-padding-left
+        // scroll-padding-right
+        // scroll-padding-top
+        // scroll-snap-align
+        // scroll-snap-stop
+        // scroll-snap-type
+        // shape-image-threshold
+        // shape-margin
+        // shape-outside
+        // speak
+        // speak-header
+        // speak-numeral
+        // speak-punctuation
+        // speech-rate
+        // stress
+        // table-layout
+        } else if name.try_match("text-align") {
+            Style::TextAlign(s.parse()?)
+        // text-combine-upright
+        // text-decoration
+        // text-decoration-color
+        // text-decoration-line
+        // text-decoration-style
+        // text-emphasis
+        // text-emphasis-color
+        // text-emphasis-position
+        // text-emphasis-style
+        // text-indent
+        // text-orientation
+        // text-overflow
+        // text-shadow
+        // text-transform
+        // text-underline-position
+        } else if name.try_match("top") {
+            Style::Top(s.parse()?)
+        // transform
+        // transform-box
+        // transform-origin
+        // unicode-bidi
+        // vertical-align
+        // visibility
+        // voice-family
+        // volume
+        } else if name.try_match("white-space") {
+            Style::WhiteSpace(s.parse()?)
+        } else if name.try_match("widows") {
+            Style::Widows(integer(s, 1..)?)
+        } else if name.try_match("width") {
+            Style::Width(s.parse()?)
+        // will-change
+        // word-spacing
+        // writing-mode
+        // z-index
+        } else {
+            return Err(name.error());
+        };
+
+        if !finished_rule(s) {
+            return Err(s.error("unexpected trailing tokens in style rule"));
+        }
+
+        Ok(output)
+    }
+}
+
+impl Parse for AlignContent {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let name: HyphenWord = s.parse()?;
+
+        if name.try_match("flex-start") {
+            Ok(AlignContent::FlexStart)
+        } else if name.try_match("flex-end") {
+            Ok(AlignContent::FlexEnd)
+        } else if name.try_match("center") {
+            Ok(AlignContent::Center)
+        } else if name.try_match("space-between") {
+            Ok(AlignContent::SpaceBetween)
+        } else if name.try_match("space-around") {
+            Ok(AlignContent::SpaceAround)
+        } else if name.try_match("stretch") {
+            Ok(AlignContent::Stretch)
+        } else {
+            Err(name.error())
+        }
+    }
+}
+
+#[test]
+fn test_align_content() {
+    for test in vec![
+        "flex-start",
+        "flex-end",
+        "center",
+        "space-between",
+        "space-around",
+        "stretch",
+    ] {
+        assert_eq!(
+            &syn::parse_str::<AlignContent>(test).unwrap().to_string(),
+            test
+        );
+    }
+    assert_eq!(
+        &syn::parse_str::<Style>("align-content:flex-start")
+            .unwrap()
+            .to_string(),
+        "align-content:flex-start"
+    );
+}
+
+impl Parse for AlignItems {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let word: HyphenWord = s.parse()?;
+        if word.try_match("normal") {
+            Ok(AlignItems::Normal)
+        } else if word.try_match("stretch") {
+            Ok(AlignItems::Stretch)
+        } else if word.try_match("center") {
+            Ok(AlignItems::Center)
+        } else if word.try_match("start") {
+            Ok(AlignItems::Start)
+        } else if word.try_match("end") {
+            Ok(AlignItems::End)
+        } else if word.try_match("flex-start") {
+            Ok(AlignItems::FlexStart)
+        } else if word.try_match("flex-end") {
+            Ok(AlignItems::FlexEnd)
+        } else if word.try_match("baseline") {
+            Ok(AlignItems::Baseline)
+        } else if word.try_match("first") {
+            let word: HyphenWord = s.parse()?;
+            if word.try_match("baseline") {
+                Ok(AlignItems::FirstBaseline)
+            } else {
+                Err(word.error())
+            }
+        } else if word.try_match("last") {
+            let word: HyphenWord = s.parse()?;
+            if word.try_match("baseline") {
+                Ok(AlignItems::LastBaseline)
+            } else {
+                Err(word.error())
+            }
+        } else if word.try_match("safe") {
+            let word: HyphenWord = s.parse()?;
+            if word.try_match("center") {
+                Ok(AlignItems::SafeCenter)
+            } else {
+                Err(word.error())
+            }
+        } else if word.try_match("unsafe") {
+            let word: HyphenWord = s.parse()?;
+            if word.try_match("center") {
+                Ok(AlignItems::UnsafeCenter)
+            } else {
+                Err(word.error())
+            }
+        } else {
+            Err(word.error())
+        }
+    }
+}
+
+impl Parse for AlignSelf {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let word: HyphenWord = s.parse()?;
+        if word.try_match("auto") {
+            Ok(AlignSelf::Auto)
+        } else if word.try_match("normal") {
+            Ok(AlignSelf::Normal)
+        } else if word.try_match("center") {
+            Ok(AlignSelf::Center)
+        } else if word.try_match("start") {
+            Ok(AlignSelf::Start)
+        } else if word.try_match("self-start") {
+            Ok(AlignSelf::SelfStart)
+        } else if word.try_match("self-end") {
+            Ok(AlignSelf::SelfEnd)
+        } else if word.try_match("flex-start") {
+            Ok(AlignSelf::FlexStart)
+        } else if word.try_match("flex-end") {
+            Ok(AlignSelf::FlexEnd)
+        } else if word.try_match("baseline") {
+            Ok(AlignSelf::Baseline)
+        } else if word.try_match("first") {
+            let word: HyphenWord = s.parse()?;
+            if word.try_match("baseline") {
+                Ok(AlignSelf::FirstBaseline)
+            } else {
+                Err(word.error())
+            }
+        } else if word.try_match("last") {
+            let word: HyphenWord = s.parse()?;
+            if word.try_match("baseline") {
+                Ok(AlignSelf::LastBaseline)
+            } else {
+                Err(word.error())
+            }
+        } else if word.try_match("stretch") {
+            Ok(AlignSelf::Stretch)
+        } else if word.try_match("safe") {
+            let word: HyphenWord = s.parse()?;
+            if word.try_match("center") {
+                Ok(AlignSelf::SafeCenter)
+            } else {
+                Err(word.error())
+            }
+        } else if word.try_match("unsafe") {
+            let word: HyphenWord = s.parse()?;
+            if word.try_match("center") {
+                Ok(AlignSelf::UnsafeCenter)
+            } else {
+                Err(word.error())
+            }
+        } else {
+            Err(word.error())
+        }
+    }
+}
+
+impl Parse for BackgroundAttachment {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let word: HyphenWord = s.parse()?;
+        if word.try_match("scroll") {
+            Ok(BackgroundAttachment::Scroll)
+        } else if word.try_match("fixed") {
+            Ok(BackgroundAttachment::Fixed)
+        } else if word.try_match("local") {
+            Ok(BackgroundAttachment::Local)
+        } else {
+            Err(word.error())
+        }
+    }
+}
+
+impl Parse for BlendMode {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let word: HyphenWord = s.parse()?;
+        if word.try_match("normal") {
+            Ok(BlendMode::Normal)
+        } else if word.try_match("multiply") {
+            Ok(BlendMode::Multiply)
+        } else if word.try_match("screen") {
+            Ok(BlendMode::Screen)
+        } else if word.try_match("overlay") {
+            Ok(BlendMode::Overlay)
+        } else if word.try_match("darken") {
+            Ok(BlendMode::Darken)
+        } else if word.try_match("lighten") {
+            Ok(BlendMode::Lighten)
+        } else if word.try_match("color-dodge") {
+            Ok(BlendMode::ColorDodge)
+        } else if word.try_match("color-burn") {
+            Ok(BlendMode::ColorBurn)
+        } else if word.try_match("hard-light") {
+            Ok(BlendMode::HardLight)
+        } else if word.try_match("soft-light") {
+            Ok(BlendMode::SoftLight)
+        } else if word.try_match("difference") {
+            Ok(BlendMode::Difference)
+        } else if word.try_match("exclusion") {
+            Ok(BlendMode::Exclusion)
+        } else if word.try_match("hue") {
+            Ok(BlendMode::Hue)
+        } else if word.try_match("saturation") {
+            Ok(BlendMode::Saturation)
+        } else if word.try_match("color") {
+            Ok(BlendMode::Color)
+        } else if word.try_match("luminosity") {
+            Ok(BlendMode::Luminosity)
+        } else {
+            Err(word.error())
+        }
+    }
+}
+
+impl Parse for BackgroundImage {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let peek = HyphenWord::peek_specific(s);
+        if peek.as_ref().map(|s| s.as_str()) == Some("url") {
+            let url;
+            syn::parenthesized!(url in s);
+            let url = url.parse::<syn::LitStr>()?;
+            Ok(BackgroundImage::Url(url.value()))
+        } else {
+            let word: HyphenWord = s.parse()?;
+            word.add_expected("url");
+            if word.try_match("none") {
+                Ok(BackgroundImage::None)
+            } else {
+                Err(word.error())
+            }
+        }
+    }
+}
+
+impl Parse for BackgroundBox {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let word: HyphenWord = s.parse()?;
+        if word.try_match("border-box") {
+            Ok(BackgroundBox::BorderBox)
+        } else if word.try_match("padding-box") {
+            Ok(BackgroundBox::PaddingBox)
+        } else if word.try_match("content-box") {
+            Ok(BackgroundBox::ContentBox)
+        } else {
+            Err(word.error())
+        }
+    }
+}
+
+impl Parse for BackgroundPosition {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let word: HyphenWord = s.parse()?;
+        if word.try_match("top") {
+            Ok(BackgroundPosition::Top)
+        } else if word.try_match("bottom") {
+            Ok(BackgroundPosition::Bottom)
+        } else if word.try_match("left") {
+            Ok(BackgroundPosition::Left)
+        } else if word.try_match("right") {
+            Ok(BackgroundPosition::Right)
+        } else if word.try_match("center") {
+            Ok(BackgroundPosition::Center)
+        } else {
+            Err(word.error())
+        }
+    }
+}
+
+impl Parse for BackgroundRepeat {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let word: HyphenWord = s.parse()?;
+        if word.try_match("repeat-x") {
+            Ok(BackgroundRepeat::RepeatX)
+        } else if word.try_match("repeat-y") {
+            Ok(BackgroundRepeat::RepeatY)
+        } else if let Ok(v) = s.parse() {
+            Ok(BackgroundRepeat::SingleOrDouble(v))
+        } else {
+            word.add_expected("repeat");
+            word.add_expected("space");
+            word.add_expected("round");
+            word.add_expected("no-repeat");
+            Err(word.error())
+        }
+    }
+}
+
+impl Parse for BgRepeatPart {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let word: HyphenWord = s.parse()?;
+        if word.try_match("repeat") {
+            Ok(BgRepeatPart::Repeat)
+        } else if word.try_match("space") {
+            Ok(BgRepeatPart::Space)
+        } else if word.try_match("round") {
+            Ok(BgRepeatPart::Round)
+        } else if word.try_match("no-repeat") {
+            Ok(BgRepeatPart::NoRepeat)
+        } else {
+            Err(word.error())
+        }
+    }
+}
+
+impl Parse for BackgroundSize {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let word: HyphenWord = s.parse()?;
+        if word.try_match("cover") {
+            Ok(BackgroundSize::Cover)
+        } else if word.try_match("contain") {
+            Ok(BackgroundSize::Contain)
+        } else if let Ok(v) = s.parse() {
+            Ok(BackgroundSize::SingleOrDouble(v))
+        } else {
+            word.add_expected("<length>");
+            word.add_expected("<percentage>");
+            word.add_expected("auto");
+            Err(word.error())
+        }
+    }
+}
+
+impl Parse for Border {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        fn line_width_error(span: Span) -> syn::Error {
+            syn::Error::new(span, "the border width was specified more than once")
+        }
+        fn line_style_error(span: Span) -> syn::Error {
+            syn::Error::new(span, "the border style was specified more than once")
+        }
+        fn color_error(span: Span) -> syn::Error {
+            syn::Error::new(span, "the border color was specified more than once")
+        }
+        let mut border = Border::new();
+        while !(border.is_full() || finished_rule(s)) {
+            let mut matched_something = false; // prevents an infinite loop when no matches
+            let width_fork = s.fork();
+            match width_fork.parse::<LineWidth>() {
+                Ok(line_width) => {
+                    if border.has_line_width() {
+                        return Err(line_width_error(width_fork.cursor().span()));
+                    }
+                    matched_something = true;
+                    border.line_width = Some(line_width);
+                    s.advance_to(&width_fork);
+                }
+                Err(_) => (),
+            }
+            let style_fork = s.fork();
+            match style_fork.parse::<LineStyle>() {
+                Ok(line_style) => {
+                    if border.has_line_style() {
+                        return Err(line_style_error(style_fork.cursor().span()));
+                    }
+                    matched_something = true;
+                    border.line_style = Some(line_style);
+                    s.advance_to(&style_fork);
+                }
+                Err(_) => (),
+            }
+            let color_fork = s.fork();
+            match color_fork.parse::<Color>() {
+                Ok(color) => {
+                    if border.has_color() {
+                        return Err(color_error(color_fork.cursor().span()));
+                    }
+                    matched_something = true;
+                    border.color = Some(color);
+                    s.advance_to(&color_fork);
+                }
+                Err(_) => (),
+            }
+            if !(matched_something || finished_rule(s)) {
+                return Err(syn::Error::new(
+                    s.cursor().span(),
+                    "unexpected input - expected one of border-width, border-style, color",
+                ));
+            }
+        }
+        Ok(border)
+    }
+}
+
+#[test]
+fn test_border_color() {
+    for (input, output) in vec![
+        ("black", Rect::All(Color::Black)),
+        (
+            "#fff blue",
+            Rect::VerticalHorizontal(Color::HexRGB(255, 255, 255), Color::Blue),
+        ),
+        (
+            "blue hsl(20, 5%, 100%) white",
+            Rect::TopHorizontalBottom(Color::Blue, Color::HSL(20.0, 5.0, 100.0), Color::White),
+        ),
+        (
+            "hsla(20, 5%, 100%, 0.2) #fff #ccc white",
+            Rect::TopRightBottomLeft(
+                Color::HSLA(20.0, 5.0, 100.0, 0.2),
+                Color::HexRGB(255, 255, 255),
+                Color::HexRGB(204, 204, 204),
+                Color::White,
+            ),
+        ),
+    ] {
+        assert_eq!(syn::parse_str::<Rect<Color>>(input).unwrap(), output);
+    }
+}
+
+#[test]
+fn test_border_width() {
+    for (input, output) in vec![
+        ("1px", BorderWidth::All(LineWidth::Length(Length::Px(1.0)))),
+        (
+            "1px 2\"em\"",
+            BorderWidth::VerticalHorizontal(
+                LineWidth::Length(Length::Px(1.0)),
+                LineWidth::Length(Length::Em(2.0)),
+            ),
+        ),
+        (
+            "2\"em\" medium thick",
+            BorderWidth::TopHorizontalBottom(
+                LineWidth::Length(Length::Em(2.0)),
+                LineWidth::Medium,
+                LineWidth::Thick,
+            ),
+        ),
+        (
+            "2\"em\" medium 1px thick",
+            BorderWidth::TopRightBottomLeft(
+                LineWidth::Length(Length::Em(2.0)),
+                LineWidth::Medium,
+                LineWidth::Length(Length::Px(1.0)),
+                LineWidth::Thick,
+            ),
+        ),
+    ] {
+        assert_eq!(syn::parse_str::<BorderWidth>(input).unwrap(), output);
+    }
+
+    for input in vec!["thi", "1px 1px 1px 1px 1px"] {
+        assert!(syn::parse_str::<BorderWidth>(input).is_err());
+    }
+}
+
+impl Parse for BorderCollapse {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let word: HyphenWord = s.parse()?;
+        if word.try_match("collapse") {
+            Ok(BorderCollapse::Collapse)
+        } else if word.try_match("separate") {
+            Ok(BorderCollapse::Separate)
+        } else {
+            Err(word.error())
+        }
+    }
+}
+
+impl Parse for BoxShadow {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        syn::custom_keyword!(none);
+        if s.peek(none) {
+            s.parse::<none>()?;
+            Ok(BoxShadow::None)
+        } else {
+            Ok(BoxShadow::Shadows(s.parse()?))
+        }
+    }
+}
+
+impl Parse for BoxSizing {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let word: HyphenWord = s.parse()?;
+        if word.try_match("border-box") {
+            Ok(BoxSizing::BorderBox)
+        } else if word.try_match("content-box") {
+            Ok(BoxSizing::ContentBox)
+        } else {
+            Err(word.error())
+        }
+    }
+}
+
+impl Parse for Clear {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let word: HyphenWord = s.parse()?;
+        if word.try_match("none") {
+            Ok(Clear::None)
+        } else if word.try_match("left") {
+            Ok(Clear::Left)
+        } else if word.try_match("right") {
+            Ok(Clear::Right)
+        } else if word.try_match("both") {
+            Ok(Clear::Both)
+        } else if word.try_match("inline-start") {
+            Ok(Clear::InlineStart)
+        } else if word.try_match("inline-end") {
+            Ok(Clear::InlineEnd)
+        } else {
+            Err(word.error())
+        }
+    }
+}
+
+impl Parse for ColumnCount {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        if s.peek(syn::LitInt) {
+            Ok(ColumnCount::Fixed(s.parse::<Integer<u32>>()?.into_inner()))
+        } else {
+            let word: HyphenWord = s.parse()?;
+            word.add_expected("integer");
+            if word.try_match("auto") {
+                Ok(ColumnCount::Auto)
+            } else {
+                Err(word.error())
+            }
+        }
+    }
+}
+
+#[test]
+fn test_clear() {
+    for (input, output) in vec![
+        ("none", Clear::None),
+        ("left", Clear::Left),
+        ("right", Clear::Right),
+        ("both", Clear::Both),
+        ("inline-start", Clear::InlineStart),
+        ("inline-end", Clear::InlineEnd),
+    ] {
+        assert_eq!(syn::parse_str::<Clear>(input).unwrap(), output);
+    }
+}
+
+impl Parse for Cursor {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let word: HyphenWord = s.parse()?;
+        if word.try_match("auto") {
+            Ok(Cursor::Auto)
+        } else if word.try_match("default") {
+            Ok(Cursor::Default)
+        } else if word.try_match("none") {
+            Ok(Cursor::None)
+        } else if word.try_match("context-menu") {
+            Ok(Cursor::ContextMenu)
+        } else if word.try_match("help") {
+            Ok(Cursor::Help)
+        } else if word.try_match("pointer") {
+            Ok(Cursor::Pointer)
+        } else if word.try_match("progress") {
+            Ok(Cursor::Progress)
+        } else if word.try_match("wait") {
+            Ok(Cursor::Wait)
+        } else if word.try_match("cell") {
+            Ok(Cursor::Cell)
+        } else if word.try_match("crosshair") {
+            Ok(Cursor::Crosshair)
+        } else if word.try_match("text") {
+            Ok(Cursor::Text)
+        } else if word.try_match("vertical-text") {
+            Ok(Cursor::VerticalText)
+        } else if word.try_match("alias") {
+            Ok(Cursor::Alias)
+        } else if word.try_match("copy") {
+            Ok(Cursor::Copy)
+        } else if word.try_match("move") {
+            Ok(Cursor::Move)
+        } else if word.try_match("no-drop") {
+            Ok(Cursor::NoDrop)
+        } else if word.try_match("not-allowed") {
+            Ok(Cursor::NotAllowed)
+        } else if word.try_match("grab") {
+            Ok(Cursor::Grab)
+        } else if word.try_match("grabbing") {
+            Ok(Cursor::Grabbing)
+        } else if word.try_match("e-resize") {
+            Ok(Cursor::EResize)
+        } else if word.try_match("n-resize") {
+            Ok(Cursor::NResize)
+        } else if word.try_match("ne-resize") {
+            Ok(Cursor::NEResize)
+        } else if word.try_match("nw-resize") {
+            Ok(Cursor::NWResize)
+        } else if word.try_match("s-resize") {
+            Ok(Cursor::SResize)
+        } else if word.try_match("se-resize") {
+            Ok(Cursor::SEResize)
+        } else if word.try_match("sw-resize") {
+            Ok(Cursor::SWResize)
+        } else if word.try_match("w-resize") {
+            Ok(Cursor::WResize)
+        } else if word.try_match("ew-resize") {
+            Ok(Cursor::EWResize)
+        } else if word.try_match("ns-resize") {
+            Ok(Cursor::NSResize)
+        } else if word.try_match("nesw-resize") {
+            Ok(Cursor::NESWResize)
+        } else if word.try_match("nwse-resize") {
+            Ok(Cursor::NWSEResize)
+        } else if word.try_match("col-resize") {
+            Ok(Cursor::ColResize)
+        } else if word.try_match("row-resize") {
+            Ok(Cursor::RowResize)
+        } else if word.try_match("all-scroll") {
+            Ok(Cursor::AllScroll)
+        } else if word.try_match("zoom-in") {
+            Ok(Cursor::ZoomIn)
+        } else if word.try_match("zoom-out") {
+            Ok(Cursor::ZoomOut)
+        } else {
+            Err(word.error())
+        }
+    }
+}
+
+impl Parse for Display {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let word: HyphenWord = s.parse()?;
+        if word.try_match("block") {
+            Ok(Display::Block)
+        } else if word.try_match("flex") {
+            Ok(Display::Flex)
+        } else if word.try_match("inline") {
+            Ok(Display::Inline)
+        } else {
+            Err(word.error())
+        }
+    }
+}
+
+impl Parse for FlexBasis {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        syn::custom_keyword!(content);
+
+        if s.peek(content) {
+            s.parse::<content>()?;
+            Ok(FlexBasis::Content)
+        } else {
+            let w: Width21 = s.parse()?;
+            Ok(FlexBasis::Width(w))
+        }
+    }
+}
+
+impl Parse for FlexDirection {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let word: HyphenWord = s.parse()?;
+        if word.try_match("column") {
+            Ok(FlexDirection::Column)
+        } else if word.try_match("row") {
+            Ok(FlexDirection::Row)
+        } else {
+            Err(word.error())
+        }
+    }
+}
+
+impl Parse for FlexWrap {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let word: HyphenWord = s.parse()?;
+        if word.try_match("wrap") {
+            Ok(FlexWrap::Wrap)
+        } else if word.try_match("nowrap") {
+            Ok(FlexWrap::Nowrap)
+        } else {
+            Err(word.error())
+        }
+    }
+}
+
+impl Parse for Float {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let word: HyphenWord = s.parse()?;
+        if word.try_match("none") {
+            Ok(Float::None)
+        } else if word.try_match("left") {
+            Ok(Float::Left)
+        } else if word.try_match("right") {
+            Ok(Float::Right)
+        } else if word.try_match("inline-start") {
+            Ok(Float::InlineStart)
+        } else if word.try_match("inline-end") {
+            Ok(Float::InlineEnd)
+        } else {
+            Err(word.error())
+        }
+    }
+}
+
+impl Parse for Font {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        if s.peek(syn::LitStr) {
+            Ok(Font::Named(s.parse::<syn::LitStr>()?.value()))
+        } else {
+            let name: HyphenWord = s.parse()?;
+            name.add_expected("named font");
+
+            if name.try_match("serif") {
+                Ok(Font::Serif)
+            } else if name.try_match("sans-serif") {
+                Ok(Font::SansSerif)
+            } else if name.try_match("cursive") {
+                Ok(Font::Cursive)
+            } else if name.try_match("fantasy") {
+                Ok(Font::Fantasy)
+            } else if name.try_match("monospace") {
+                Ok(Font::Fantasy)
+            } else {
+                Err(name.error())
+            }
+        }
+    }
+}
+
+#[test]
+fn test_font_family() {
+    for (input, output) in vec![
+        (
+            "cursive",
+            FontFamily {
+                first: Font::Cursive,
+                rest: vec![],
+            },
+        ),
+        (
+            "\"Amatic SC\", sans-serif",
+            FontFamily {
+                first: Font::Named("Amatic SC".to_string()),
+                rest: vec![Font::SansSerif],
+            },
+        ),
+    ] {
+        assert_eq!(syn::parse_str::<FontFamily>(input).unwrap(), output);
+    }
+
+    for val in vec![
+        "font-family:\"Font Awesome 5 Free\"",
+        "font-family:\"Some Name\",\"Another Name\",serif",
+    ] {
+        assert_eq!(&syn::parse_str::<Style>(val).unwrap().to_string(), val);
+    }
+}
+
+impl Parse for FontSize {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let word_fork = s.fork();
+        let name: HyphenWord = word_fork.parse()?;
+
+        if name.try_match("xx-small") {
+            s.advance_to(&word_fork);
+            Ok(FontSize::XXSmall)
+        } else if name.try_match("x-small") {
+            s.advance_to(&word_fork);
+            Ok(FontSize::XSmall)
+        } else if name.try_match("small") {
+            s.advance_to(&word_fork);
+            Ok(FontSize::Small)
+        } else if name.try_match("medium") {
+            s.advance_to(&word_fork);
+            Ok(FontSize::Medium)
+        } else if name.try_match("large") {
+            s.advance_to(&word_fork);
+            Ok(FontSize::Large)
+        } else if name.try_match("x-large") {
+            s.advance_to(&word_fork);
+            Ok(FontSize::XLarge)
+        } else if name.try_match("xx-large") {
+            s.advance_to(&word_fork);
+            Ok(FontSize::XXLarge)
+        } else if name.try_match("xxx-large") {
+            s.advance_to(&word_fork);
+            Ok(FontSize::XXXLarge)
+        } else if name.try_match("larger") {
+            s.advance_to(&word_fork);
+            Ok(FontSize::Larger)
+        } else if name.try_match("smaller") {
+            s.advance_to(&word_fork);
+            Ok(FontSize::Smaller)
+        } else {
+            s.parse().map(FontSize::LengthPercentage).map_err(|_| {
+                name.add_expected("length");
+                name.add_expected("percentage");
+                name.error()
+            })
+        }
+    }
+}
+impl Parse for FontStyle {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let name: HyphenWord = s.parse()?;
+
+        if name.try_match("normal") {
+            Ok(FontStyle::Normal)
+        } else if name.try_match("italic") {
+            Ok(FontStyle::Italic)
+        } else if name.try_match("oblique") {
+            Ok(FontStyle::Oblique)
+        } else {
+            Err(name.error())
+        }
+    }
+}
+
+#[test]
+fn test_font_style() {
+    for (input, output) in vec![
+        ("normal", FontStyle::Normal),
+        ("italic", FontStyle::Italic),
+        ("oblique", FontStyle::Oblique),
+    ] {
+        assert_eq!(syn::parse_str::<FontStyle>(input).unwrap(), output);
+    }
+
+    for input in vec!["norma", "normal trailing"] {
+        assert!(syn::parse_str::<FontStyle>(input).is_err());
+    }
+}
+
+impl Parse for FontWeight {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let name: HyphenWord = s.parse()?;
+        name.add_expected("number where 1 <= number <= 1000");
+
+        if name.try_match("normal") {
+            Ok(FontWeight::Normal)
+        } else if name.try_match("bold") {
+            Ok(FontWeight::Bold)
+        } else if name.try_match("lighter") {
+            Ok(FontWeight::Lighter)
+        } else if name.try_match("bolder") {
+            Ok(FontWeight::Bolder)
+        } else {
+            let n: Number = s.parse().map_err(|_| name.error())?;
+            if n.suffix.is_empty() && n.value >= 1.0 && n.value <= 1000.0 {
+                Ok(FontWeight::Number(n.value))
+            } else {
+                Err(name.error())
+            }
+        }
+    }
+}
+
+#[test]
+fn test_font_weight() {
+    for (input, output) in vec![
+        ("normal", FontWeight::Normal),
+        ("bold", FontWeight::Bold),
+        ("lighter", FontWeight::Lighter),
+        ("bolder", FontWeight::Bolder),
+        ("1", FontWeight::Number(1.0)),
+        ("1.0", FontWeight::Number(1.0)),
+        ("1000", FontWeight::Number(1000.0)),
+        ("1000.0", FontWeight::Number(1000.0)),
+        ("246.15", FontWeight::Number(246.15)),
+    ] {
+        match syn::parse_str::<FontWeight>(input) {
+            Ok(v) => assert_eq!(v, output),
+            Err(e) => panic!("error parsing {}: {}", input, e),
+        }
+    }
+}
+
+impl Parse for JustifyContent {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let name: HyphenWord = s.parse()?;
+
+        if name.try_match("flex-start") {
+            Ok(JustifyContent::FlexStart)
+        } else if name.try_match("flex-end") {
+            Ok(JustifyContent::FlexEnd)
+        } else if name.try_match("center") {
+            Ok(JustifyContent::Center)
+        } else if name.try_match("space-between") {
+            Ok(JustifyContent::SpaceBetween)
+        } else if name.try_match("space-around") {
+            Ok(JustifyContent::SpaceAround)
+        } else if name.try_match("start") {
+            // - not in level 1 spec
+            Ok(JustifyContent::FlexStart)
+        } else if name.try_match("end") {
+            // - not in level 1 spec
+            Ok(JustifyContent::FlexEnd)
+        } else {
+            Err(name.error())
+        }
+    }
+}
+
+impl Parse for Length {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let neg = if s.peek(Token![-]) {
+            s.parse::<Token![-]>()?;
+            true
+        } else {
+            false
+        };
+        let n: Number = s.parse()?;
+        Length::parse_from_number(n, neg)
+    }
+}
+
+impl Length {
+    fn parse_from_number(n: Number, neg: bool) -> syn::Result<Self> {
+        let neg = if neg { -1.0 } else { 1.0 };
+        if n.suffix == "em" {
+            Ok(Length::Em(n.value * neg))
+        } else if n.suffix == "ex" {
+            Ok(Length::Ex(n.value * neg))
+        } else if n.suffix == "in" {
+            Ok(Length::In(n.value * neg))
+        } else if n.suffix == "cm" {
+            Ok(Length::Cm(n.value * neg))
+        } else if n.suffix == "mm" {
+            Ok(Length::Mm(n.value * neg))
+        } else if n.suffix == "pt" {
+            Ok(Length::Pt(n.value * neg))
+        } else if n.suffix == "pc" {
+            Ok(Length::Pc(n.value * neg))
+        } else if n.suffix == "px" {
+            Ok(Length::Px(n.value * neg))
+        } else if n.suffix == "" && n.value == 0.0 {
+            Ok(Length::Zero)
+        } else {
+            // No matches so return error
+            Err(syn::Error::new(
+                n.span,
+                "expected one of `\"em\"`, `\"ex\"`, `in`, `cm`, `mm`, `pt`, `pc`, `px` after number, or 0",
+            ))
+        }
+    }
+}
+
+impl Parse for LineStyle {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let name = s.parse::<HyphenWord>()?;
+        if name.try_match("none") {
+            Ok(LineStyle::None)
+        } else if name.try_match("hidden") {
+            Ok(LineStyle::Hidden)
+        } else if name.try_match("dotted") {
+            Ok(LineStyle::Dotted)
+        } else if name.try_match("dashed") {
+            Ok(LineStyle::Dashed)
+        } else if name.try_match("solid") {
+            Ok(LineStyle::Solid)
+        } else if name.try_match("double") {
+            Ok(LineStyle::Double)
+        } else if name.try_match("groove") {
+            Ok(LineStyle::Groove)
+        } else if name.try_match("ridge") {
+            Ok(LineStyle::Ridge)
+        } else if name.try_match("inset") {
+            Ok(LineStyle::Inset)
+        } else if name.try_match("outset") {
+            Ok(LineStyle::Outset)
+        } else {
+            Err(name.error())
+        }
+    }
+}
+
+impl Parse for LineWidth {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let name = s.parse::<HyphenWord>()?;
+        if name.try_match("thin") {
+            Ok(LineWidth::Thin)
+        } else if name.try_match("medium") {
+            Ok(LineWidth::Medium)
+        } else if name.try_match("thick") {
+            Ok(LineWidth::Thick)
+        } else {
+            match s.parse::<Length>() {
+                Ok(l) => Ok(LineWidth::Length(l)),
+                Err(_) => {
+                    name.add_expected("length");
+                    Err(name.error())
+                }
+            }
+        }
+    }
+}
+
+#[test]
+fn test_parse_line_width() {
+    assert_eq!(
+        syn::parse_str::<LineWidth>("thin").unwrap(),
+        LineWidth::Thin
+    );
+}
+
+impl Parse for LineHeight {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        Ok(LineHeight(s.parse::<syn::LitFloat>()?.base10_parse()?))
+    }
+}
+
+impl Parse for ListStyleType {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let name: HyphenWord = s.parse()?;
+
+        if name.try_match("disc") {
+            Ok(ListStyleType::Disc)
+        } else if name.try_match("circle") {
+            Ok(ListStyleType::Circle)
+        } else if name.try_match("square") {
+            Ok(ListStyleType::Square)
+        } else if name.try_match("decimal") {
+            Ok(ListStyleType::Decimal)
+        } else if name.try_match("decimal-leading-zero") {
+            Ok(ListStyleType::DecimalLeadingZero)
+        } else if name.try_match("lower-roman") {
+            Ok(ListStyleType::LowerRoman)
+        } else if name.try_match("upper-roman") {
+            Ok(ListStyleType::UpperRoman)
+        } else if name.try_match("lower-greek") {
+            Ok(ListStyleType::LowerGreek)
+        } else if name.try_match("upper-greek") {
+            Ok(ListStyleType::UpperGreek)
+        } else if name.try_match("lower-latin") {
+            Ok(ListStyleType::LowerLatin)
+        } else if name.try_match("upper-latin") {
+            Ok(ListStyleType::UpperLatin)
+        } else if name.try_match("armenian") {
+            Ok(ListStyleType::Armenian)
+        } else if name.try_match("georgian") {
+            Ok(ListStyleType::Georgian)
+        } else if name.try_match("lower-alpha") {
+            Ok(ListStyleType::LowerAlpha)
+        } else if name.try_match("upper-alpha") {
+            Ok(ListStyleType::UpperAlpha)
+        } else if name.try_match("none") {
+            Ok(ListStyleType::None)
+        } else {
+            Err(name.error())
+        }
+    }
+}
+
+impl Parse for MaxWidthHeight {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let name = s.parse::<HyphenWord>()?;
+        name.add_expected("length");
+        name.add_expected("percentage");
+        if name.try_match("none") {
+            Ok(MaxWidthHeight::None)
+        } else if name.try_match("min-content") {
+            Ok(MaxWidthHeight::MinContent)
+        } else if name.try_match("max-content") {
+            Ok(MaxWidthHeight::MaxContent)
+        } else if name.try_match("fit-content") {
+            let content;
+            syn::parenthesized!(content in s);
+            Ok(MaxWidthHeight::FitContent(content.parse()?))
+        } else {
+            s.parse()
+                .map(|lp| MaxWidthHeight::LengthPercentage(lp))
+                .map_err(|_| name.error())
+        }
+    }
+}
+
+#[test]
+fn test_max_width_height() {
+    let style: Style = syn::parse_str("max-width: 200px").unwrap();
+    assert_eq!(&style.to_string(), "max-width:200px");
+}
+
+impl<T> Parse for Rect<T>
+where
+    T: Parse,
+{
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let first = s.parse::<T>()?;
+        let fork = s.fork();
+        let second = match fork.parse::<T>() {
+            Ok(v) => {
+                s.advance_to(&fork);
+                v
+            }
+            Err(_) => return Ok(Rect::All(first)),
+        };
+        let third = match fork.parse::<T>() {
+            Ok(v) => {
+                s.advance_to(&fork);
+                v
+            }
+            Err(_) => return Ok(Rect::VerticalHorizontal(first, second)),
+        };
+        match fork.parse::<T>() {
+            Ok(v) => {
+                s.advance_to(&fork);
+                Ok(Rect::TopRightBottomLeft(first, second, third, v))
+            }
+            Err(_) => Ok(Rect::TopHorizontalBottom(first, second, third)),
+        }
+    }
+}
+
+impl Parse for AutoLengthPercentage {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        syn::custom_keyword!(auto);
+        if s.peek(auto) {
+            s.parse::<auto>()?;
+            Ok(AutoLengthPercentage::Auto)
+        } else {
+            Ok(AutoLengthPercentage::LengthPercentage(s.parse()?))
+        }
+    }
+}
+
+impl Parse for ObjectFit {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let name: HyphenWord = s.parse()?;
+        if name.try_match("fill") {
+            Ok(ObjectFit::Fill)
+        } else if name.try_match("none") {
+            Ok(ObjectFit::None)
+        } else if name.try_match("contain") {
+            if s.is_empty() {
+                Ok(ObjectFit::Contain { scale_down: false })
+            } else {
+                let scale_down_word: HyphenWord = s.parse()?;
+                if scale_down_word.try_match("scale-down") {
+                    Ok(ObjectFit::Contain { scale_down: true })
+                } else {
+                    Err(scale_down_word.error())
+                }
+            }
+        } else if name.try_match("cover") {
+            if HyphenWord::peek(s) {
+                let scale_down_word: HyphenWord = s.parse()?;
+                if scale_down_word.try_match("scale-down") {
+                    Ok(ObjectFit::Cover { scale_down: true })
+                } else {
+                    Err(scale_down_word.error())
+                }
+            } else {
+                Ok(ObjectFit::Cover { scale_down: false })
+            }
+        } else if name.try_match("scale-down") {
+            if HyphenWord::peek(s) {
+                let cover_contain: HyphenWord = s.parse()?;
+                if cover_contain.try_match("cover") {
+                    Ok(ObjectFit::Cover { scale_down: true })
+                } else if cover_contain.try_match("contain") {
+                    Ok(ObjectFit::Contain { scale_down: true })
+                } else {
+                    Err(cover_contain.error())
+                }
+            } else {
+                // defaults to contain when cover/contain not present
+                Ok(ObjectFit::Contain { scale_down: true })
+            }
+        } else {
+            Err(name.error())
+        }
+    }
+}
+
+impl Parse for Overflow {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let first = s.parse::<OverflowXY>()?;
+        Ok(match s.parse::<OverflowXY>() {
+            Ok(second) => Overflow::XY(first, second),
+            Err(_) => Overflow::Both(first),
+        })
+    }
+}
+
+impl Parse for OverflowXY {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let name: HyphenWord = s.parse()?;
+
+        if name.try_match("visible") {
+            Ok(OverflowXY::Visible)
+        } else if name.try_match("hidden") {
+            Ok(OverflowXY::Hidden)
+        } else if name.try_match("clip") {
+            Ok(OverflowXY::Clip)
+        } else if name.try_match("scroll") {
+            Ok(OverflowXY::Scroll)
+        } else if name.try_match("auto") {
+            Ok(OverflowXY::Auto)
+        } else {
+            Err(name.error())
+        }
+    }
+}
+
+impl Parse for Position {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let name: HyphenWord = s.parse()?;
+        if name.try_match("static") {
+            Ok(Position::Static)
+        } else if name.try_match("relative") {
+            Ok(Position::Relative)
+        } else if name.try_match("absolute") {
+            Ok(Position::Absolute)
+        } else if name.try_match("fixed") {
+            Ok(Position::Fixed)
+        } else {
+            Err(name.error())
+        }
+    }
+}
+
+#[test]
+fn test_padding() {
+    for (input, output) in vec![(
+        "padding:1\"em\"",
+        Style::Padding(Padding::All(Calc::Normal(LengthPercentage::Length(
+            Length::Em(1.0),
+        )))),
+    )] {
+        assert_eq!(syn::parse_str::<Style>(input).unwrap(), output);
+    }
+}
+
+impl Parse for Percentage {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let n: Number = s.parse()?;
+        if n.suffix == "%" {
+            Ok(Percentage(n.value))
+        } else {
+            Err(syn::Error::new(n.span, "expected percentage"))
+        }
+    }
+}
+
+impl Parse for WhiteSpace {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let name: HyphenWord = s.parse()?;
+        if name.try_match("normal") {
+            Ok(WhiteSpace::Normal)
+        } else if name.try_match("pre") {
+            Ok(WhiteSpace::Pre)
+        } else if name.try_match("nowrap") {
+            Ok(WhiteSpace::Nowrap)
+        } else if name.try_match("pre-wrap") {
+            Ok(WhiteSpace::PreWrap)
+        } else if name.try_match("pre-line") {
+            Ok(WhiteSpace::PreLine)
+        } else {
+            Err(name.error())
+        }
+    }
+}
+
+impl Parse for Width21 {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        syn::custom_keyword!(auto);
+
+        if s.peek(auto) {
+            s.parse::<auto>()?;
+            Ok(Width21::Auto)
+        } else {
+            Ok(Width21::LengthPercentage(s.parse()?))
+        }
+    }
+}
+
+impl Parse for WidthHeight {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let fork = s.fork();
+        let name: HyphenWord = fork.parse()?;
+
+        if name.try_match("auto") {
+            s.advance_to(&fork);
+            Ok(WidthHeight::Auto)
+        } else if name.try_match("min-content") {
+            s.advance_to(&fork);
+            Ok(WidthHeight::MinContent)
+        } else if name.try_match("max-content") {
+            s.advance_to(&fork);
+            Ok(WidthHeight::MaxContent)
+        } else if name.try_match("fit-content") {
+            s.advance_to(&fork);
+            let content;
+            syn::parenthesized!(content in s);
+            let lp = content.parse()?;
+            if !content.is_empty() {
+                Err(content.error("trailing tokens"))
+            } else {
+                Ok(WidthHeight::FitContent(lp))
+            }
+        } else {
+            // todo error message
+            Ok(WidthHeight::LengthPercentage(s.parse()?))
+        }
+    }
+}
+
+#[test]
+fn test_width_height() {
+    for (input, output) in vec![
+        ("0", "0"),
+        ("1px", "1px"),
+        ("1\"em\"", "1em"),
+        ("calc(100% - 60px)", "calc(100% - 60px)"),
+    ] {
+        match syn::parse_str::<WidthHeight>(input) {
+            Ok(v) => assert_eq!(&v.to_string(), output),
+            Err(e) => panic!("Error in \"{}\": {}", input, e),
+        }
+    }
+}
+
+impl Parse for LengthPercentage {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        if s.peek2(Token![%]) {
+            Ok(LengthPercentage::Percentage(s.parse()?))
+        } else {
+            Ok(LengthPercentage::Length(s.parse()?))
+        }
+    }
+}
+
+#[test]
+fn test_length_percentage() {
+    for (input, output) in vec![
+        ("1\"em\"", LengthPercentage::Length(Length::Em(1.0))),
+        ("1.0px", LengthPercentage::Length(Length::Px(1.0))),
+        ("0", LengthPercentage::Length(Length::Zero)),
+    ] {
+        assert_eq!(syn::parse_str::<LengthPercentage>(input).unwrap(), output);
+    }
+}
+
+impl Parse for Resize {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let name: HyphenWord = s.parse()?;
+
+        if name.try_match("none") {
+            Ok(Resize::None)
+        } else if name.try_match("both") {
+            Ok(Resize::Both)
+        } else if name.try_match("horizontal") {
+            Ok(Resize::Horizontal)
+        } else if name.try_match("vertical") {
+            Ok(Resize::Vertical)
+        } else {
+            Err(name.error())
+        }
+    }
+}
+
+impl Parse for Shadow {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        syn::custom_keyword!(inset);
+        let mut inset_val = false;
+        let mut length: Option<ShadowLength> = None;
+        let mut color: Option<Color> = None;
+        // keep trying all three until we're done or there is an error
+        loop {
+            let mut parsed_something = false;
+            // inset (easiest)
+            if s.peek(inset) {
+                let inset_tok = s.parse::<inset>()?;
+                if inset_val {
+                    return Err(syn::Error::new(
+                        inset_tok.span(),
+                        "`inset` must be specified 0 or 1 times",
+                    ));
+                }
+                inset_val = true;
+                parsed_something = true;
+            }
+
+            // color
+            let fork = s.fork();
+            if let Ok(parsed_color) = fork.parse::<Color>() {
+                if color.is_some() {
+                    return Err(s.error("color must be specified 0 or 1 times"));
+                }
+                color = Some(parsed_color);
+                s.advance_to(&fork);
+                parsed_something = true;
+            }
+
+            // length
+            let fork = s.fork();
+            if let Ok(parsed_length) = fork.parse::<ShadowLength>() {
+                if length.is_some() {
+                    return Err(s.error("shadow length must be specified once"));
+                }
+                length = Some(parsed_length);
+                s.advance_to(&fork);
+                parsed_something = true;
+            }
+
+            // if we've failed to parse anything, end the loop.
+            if !parsed_something {
+                break;
+            }
+        }
+        if let Some(length) = length {
+            Ok(Shadow {
+                color,
+                length,
+                inset: inset_val,
+            })
+        } else {
+            Err(s.error("expected color, length, or `inset`"))
+        }
+    }
+}
+
+impl Parse for ShadowLength {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let horizontal: Length = s.parse()?;
+        let vertical: Length = s.parse()?;
+
+        // blur
+        let fork = s.fork();
+        let blur = match fork.parse::<Length>() {
+            Ok(blur) => {
+                s.advance_to(&fork);
+                blur
+            }
+            Err(_) => {
+                return Ok(ShadowLength::Offsets {
+                    horizontal,
+                    vertical,
+                });
+            }
+        };
+
+        // spread
+        let fork = s.fork();
+        match fork.parse::<Length>() {
+            Ok(spread) => {
+                s.advance_to(&fork);
+
+                Ok(ShadowLength::OffsetsBlurSpread {
+                    horizontal,
+                    vertical,
+                    blur,
+                    spread,
+                })
+            }
+            Err(_) => Ok(ShadowLength::OffsetsBlur {
+                horizontal,
+                vertical,
+                blur,
+            }),
+        }
+    }
+}
+
+impl Parse for TextAlign {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let word: HyphenWord = s.parse()?;
+        if word.try_match("left") {
+            Ok(TextAlign::Left)
+        } else if word.try_match("right") {
+            Ok(TextAlign::Right)
+        } else if word.try_match("center") {
+            Ok(TextAlign::Center)
+        } else if word.try_match("justify") {
+            Ok(TextAlign::Justify)
+        } else {
+            Err(word.error())
+        }
+    }
+}
+
+// color
+// =====
+
+impl Parse for DynamicColor {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        Ok(if s.peek(syn::token::Brace) {
+            DynamicColor::Dynamic(s.parse()?)
+        } else {
+            DynamicColor::Literal(s.parse()?)
+        })
+    }
+}
+
+impl Parse for Color {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        if s.peek(Token![#]) {
+            return parse_hex_color(s);
+        }
+        let fn_name: HyphenWord = s.parse()?;
+        if fn_name.try_match("hsl") {
+            parse_hsl_color(s, false)
+        } else if fn_name.try_match("hsla") {
+            parse_hsl_color(s, true)
+        } else {
+            if let Some(name) = fn_name.word.as_ref() {
+                if let Some(color) = Color::from_named(name) {
+                    return Ok(color);
+                }
+            }
+            fn_name.add_expected("named color");
+            Err(fn_name.error())
+        }
+    }
+}
+
+fn parse_hex_color(s: ParseStream) -> syn::Result<Color> {
+    const ERR_MSG: &'static str = "to avoid confusing rust, please enclose hex colors in `\"`";
+    s.parse::<Token![#]>()?;
+    if !(s.peek(syn::LitStr) || s.peek(Ident)) {
+        return Err(s.error(ERR_MSG));
+    }
+    if s.peek(syn::LitStr) {
+        let hex_str: syn::LitStr = s.parse()?;
+        color::parse_hex(&hex_str.value()).ok_or(syn::Error::new(hex_str.span(), ERR_MSG))
+    } else {
+        let hex_str: Ident = s.parse()?;
+        color::parse_hex(&hex_str.to_string()).ok_or(syn::Error::new(hex_str.span(), ERR_MSG))
+    }
+}
+
+fn parse_hsl_color(s: ParseStream, with_alpha: bool) -> syn::Result<Color> {
+    let content;
+    syn::parenthesized!(content in s);
+    let n: Number = content.parse()?;
+    n.empty_suffix()?;
+    let hue = n.value;
+    if hue < 0.0 || hue >= 360.0 {
+        return Err(syn::Error::new(
+            n.span,
+            "hue should be in the range `0 <= hue < 360`",
+        ));
+    }
+    content.parse::<Token![,]>()?;
+    let n: Number = content.parse()?;
+    if n.suffix != "%" {
+        return Err(syn::Error::new(
+            n.span,
+            "saturation should be a percentage (followed by `%`)",
+        ));
+    }
+    let sat = n.value;
+    if sat < 0.0 || sat > 100.0 {
+        return Err(syn::Error::new(
+            n.span,
+            "saturation should be in the range `0 <= sat < 100`",
+        ));
+    }
+    content.parse::<Token![,]>()?;
+    let n: Number = content.parse()?;
+    if n.suffix != "%" {
+        return Err(syn::Error::new(
+            n.span,
+            "saturation should be a percentage (followed by `%`)",
+        ));
+    }
+    let light = n.value;
+    if light < 0.0 || light > 100.0 {
+        return Err(syn::Error::new(
+            n.span,
+            "lightness should be in the range `0 <= light < 100`",
+        ));
+    }
+    // since we parse content in parentheses, we can assume no trailing characers
+    if !with_alpha {
+        return if content.is_empty() {
+            Ok(Color::HSL(hue, sat, light))
+        } else {
+            Err(content.error("trailing characters"))
+        };
+    }
+    // we are a hsla
+    content.parse::<Token![,]>()?;
+    let n: Number = content.parse()?;
+    n.empty_suffix()?;
+    let alpha = n.value;
+    if alpha < 0.0 || alpha > 1.0 {
+        return Err(syn::Error::new(
+            n.span,
+            "alpha should be in the range `0 <= alpha < 1`",
+        ));
+    }
+    if content.is_empty() {
+        Ok(Color::HSLA(hue, sat, light, alpha))
+    } else {
+        Err(content.error("unexpected trailing characters"))
+    }
+}
+
+#[test]
+fn test_color() {
+    for (input, output) in vec![
+        ("#ffffffff", Color::HexRGBA(255, 255, 255, 255)),
+        ("#ffffff", Color::HexRGB(255, 255, 255)),
+        ("#fff", Color::HexRGB(255, 255, 255)),
+        ("#\"fff\"", Color::HexRGB(255, 255, 255)),
+        ("hsl(100, 50%, 50%)", Color::HSL(100.0, 50.0, 50.0)),
+        ("hsla(60, 0%, 0%, 0.2)", Color::HSLA(60.0, 0.0, 0.0, 0.2)),
+        ("black", Color::Black),
+        ("yellow", Color::Yellow),
+    ] {
+        match syn::parse_str::<Color>(input) {
+            Ok(c) => assert_eq!(c, output),
+            Err(e) => panic!("error parsing color {}: {}", input, e),
+        }
+    }
+}
+
+// Util
+// ====
+
+impl<T> Parse for NonemptyCommaList<T>
+where
+    T: Parse,
+{
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let punctuated = Punctuated::<T, Token![,]>::parse_separated_nonempty(s)?;
+        let mut iter = punctuated.into_iter();
+        let first = iter.next().unwrap();
+        Ok(Self {
+            first,
+            rest: iter.collect(),
+        })
+    }
+}
+
+impl<T> Parse for SingleOrDouble<T>
+where
+    T: Parse,
+{
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let first = T::parse(s)?;
+        let fork = s.fork();
+        Ok(match T::parse(&fork) {
+            Ok(second) => {
+                s.advance_to(&fork);
+                SingleOrDouble::Double {
+                    vert: first,
+                    horiz: second,
+                }
+            }
+            Err(_) => SingleOrDouble::Single(first),
+        })
+    }
+}
+
+/// Either a float or an int, converted in either case to f64.
+///
+/// A trailing percent (`%`) character will be consumed if the number has no suffix. This is valid
+/// according to the CSS tokeniser spec.
+///
+/// TODO This only works for floats for now. Although JS only supports floats, integer literals are
+/// used in css.
+#[derive(Debug)]
+struct Number {
+    value: f64,
+    suffix: String,
+    span: Span,
+}
+
+impl Number {
+    fn empty_suffix(&self) -> syn::Result<()> {
+        if self.suffix != "" {
+            Err(syn::Error::new(
+                self.span,
+                "unexpected characters after number",
+            ))
+        } else {
+            Ok(())
+        }
+    }
+
+    #[cfg(test)]
+    fn check_value(&self, value: f64, suffix: &str) -> bool {
+        self.value == value && self.suffix == suffix
+    }
+}
+
+impl Parse for Number {
+    fn parse(s: ParseStream) -> syn::Result<Number> {
+        let lookahead = s.lookahead1();
+        let (value, mut span, mut suffix) = if lookahead.peek(syn::LitFloat) {
+            let tok = s.parse::<syn::LitFloat>()?;
+            let num = tok.base10_parse()?;
+            (num, tok.span(), tok.suffix().to_string())
+        } else if lookahead.peek(syn::LitInt) {
+            let tok = s.parse::<syn::LitInt>()?;
+            // u32 chosen because it can be safely converted into f64
+            let num = tok.base10_parse::<u32>()?;
+            (num.into(), tok.span(), tok.suffix().to_string())
+        } else {
+            return Err(lookahead.error());
+        };
+        if suffix.is_empty() {
+            // look for a `%` for the suffix
+            if s.peek(Token![%]) {
+                let tok = s.parse::<Token![%]>()?;
+                if let Some(extra_span) = span.join(tok.span) {
+                    span = extra_span;
+                }
+                suffix.push('%');
+            // work-around using literal strings because the lexer can't support suffixes beginning
+            // with `e` for floats: https://github.com/rust-lang/rust/issues/67544
+            } else if s.peek(syn::LitStr) {
+                let tok = s.parse::<syn::LitStr>()?;
+                if let Some(extra_span) = span.join(tok.span()) {
+                    span = extra_span;
+                }
+                suffix.push_str(&tok.value());
+            }
+        }
+        Ok(Number {
+            value,
+            suffix,
+            span,
+        })
+    }
+}
+
+#[test]
+fn test_number() {
+    for (input, value, suffix) in vec![
+        ("200", 200.0, ""),
+        ("200.0", 200.0, ""),
+        ("0", 0.0, ""),
+        ("0in", 0.0, "in"),
+    ] {
+        assert!(syn::parse_str::<Number>(input)
+            .unwrap()
+            .check_value(value, suffix),)
+    }
+}
+
+/// Something like `word-separated-hyphens`
+#[derive(Debug)]
+struct HyphenWord {
+    pub span: Span,
+    pub word: Option<String>,
+    /// List of tried matches - for building error.
+    tried: TryList,
+}
+
+impl HyphenWord {
+    pub fn new(span: Span, word: String) -> Self {
+        HyphenWord {
+            span,
+            word: Some(word),
+            tried: TryList::new(),
+        }
+    }
+
+    /// This allows HyphenWords to be empty. In this case the token cursor will not advance and the
+    /// returned word will be blank.
+    pub fn new_no_word(span: Span) -> Self {
+        HyphenWord {
+            span,
+            word: None,
+            tried: TryList::new(),
+        }
+    }
+
+    pub fn try_match(&self, other: &str) -> bool {
+        if Some(other) == self.word.as_ref().map(|s| s.as_str()) {
+            true
+        } else {
+            self.tried.add_literal(other);
+            false
+        }
+    }
+
+    pub fn add_expected(&self, ty: &str) {
+        self.tried.add(ty);
+    }
+
+    /// Panics if there were no calls to `try_match` before calling this function.
+    pub fn error(&self) -> syn::Error {
+        self.tried.to_error(self.span)
+    }
+
+    /// This is cheaper than peek-specific
+    pub fn peek(s: ParseStream) -> bool {
+        s.peek(Ident)
+    }
+
+    /// Peek the next HyphenWord without advancing the parser.
+    pub fn peek_specific(s: ParseStream) -> Option<String> {
+        let fork = s.fork();
+        match HyphenWord::parse(&fork) {
+            Ok(hw) => Some(hw.word.unwrap()),
+            Err(_) => None,
+        }
+    }
+}
+
+impl Parse for HyphenWord {
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        let fork = s.fork();
+        let first = match fork.call(Ident::parse_any) {
+            Ok(v) => {
+                s.advance_to(&fork);
+                v
+            }
+            Err(_) => return Ok(HyphenWord::new_no_word(s.cursor().span())),
+        };
+        let mut word = first.to_string();
+        let mut span = first.span();
+        // This is potentially unbounded. Probably not be a problem but making a note anyway.
+        while s.peek(Token![-]) {
+            let hyphen = s.parse::<Token![-]>()?;
+            if let Some(joined) = span.join(hyphen.span) {
+                span = joined;
+            }
+            let part = s.call(Ident::parse_any)?;
+            write!(word, "-{}", part).unwrap();
+            if let Some(joined) = span.join(part.span()) {
+                span = joined;
+            }
+        }
+        Ok(HyphenWord::new(span, word))
+    }
+}
+
+#[test]
+fn test_hyphen_word() {
+    let word: HyphenWord = syn::parse_str("first-second-third").unwrap();
+    assert_eq!(word.word, Some("first-second-third".to_string()));
+    assert!(syn::parse_str::<HyphenWord>("first-second-").is_err());
+    assert!(syn::parse_str::<HyphenWord>("a a").is_err());
+}
+
+/// Keeps track of a list of tokens that have been tried.
+#[derive(Debug)]
+pub struct TryList(RefCell<BTreeSet<String>>);
+
+impl TryList {
+    pub fn new() -> Self {
+        TryList(RefCell::new(BTreeSet::new()))
+    }
+
+    /// Same as add, but with quotes
+    pub fn add_literal(&self, lit: &str) {
+        self.add(format!("`{}`", lit));
+    }
+
+    pub fn add(&self, ty: impl Into<String>) {
+        self.0.borrow_mut().insert(ty.into());
+    }
+
+    fn to_error(&self, span: Span) -> syn::Error {
+        let tried = self.0.borrow();
+        let mut iter = tried.iter();
+        let start = iter.next().unwrap().to_owned();
+        let list = iter.fold(start, |mut acc, itm| {
+            write!(acc, ", {}", itm).unwrap();
+            acc
+        });
+        let error_msg = format!("expected one of {}", list);
+        syn::Error::new(span, error_msg)
+    }
+}
+
+/// Whether we are at the end of a rule. Either the stream will be empty, or there will be a
+/// semi-colon.
+fn finished_rule(s: ParseStream) -> bool {
+    s.is_empty() || s.peek(Token![;])
+}
+
+// Parsing integers
+
+#[derive(Debug, PartialEq)]
+struct Integer<T> {
+    value: T,
+}
+
+impl<T> Integer<T> {
+    fn into_inner(self) -> T {
+        self.value
+    }
+}
+
+impl<T> Parse for Integer<T>
+where
+    T: str::FromStr + fmt::Display + PartialOrd<T>,
+    <T as str::FromStr>::Err: fmt::Display,
+{
+    fn parse(s: ParseStream) -> syn::Result<Self> {
+        Ok(Integer {
+            value: integer(s, ..)?,
+        })
+    }
+}
+
+/// Parse an integer, with an optional allowed range.
+fn integer<T, R>(s: ParseStream, range: R) -> syn::Result<T>
+where
+    R: RangeBounds<T> + fmt::Debug,
+    T: str::FromStr + fmt::Display + PartialOrd<T>,
+    <T as str::FromStr>::Err: fmt::Display,
+{
+    let fixed = s.parse::<syn::LitInt>()?;
+    let span = fixed.span();
+    if fixed.suffix().is_empty() {
+        let fixed = fixed.base10_parse()?;
+        if range.contains(&fixed) {
+            Ok(fixed)
+        } else {
+            Err(syn::Error::new(
+                span,
+                format!(
+                    "expected a number in the range {:?}, found {}",
+                    range, fixed
+                ),
+            ))
+        }
+    } else {
+        Err(syn::Error::new(span, "the number should not have a suffix"))
+    }
+}
+
+#[test]
+fn test_parse_integer() {
+    let x: Integer<u8> = syn::parse_str("123").unwrap();
+    assert_eq!(x.into_inner(), 123);
+    let x: syn::Result<Integer<u8>> = syn::parse_str("256");
+    assert!(x.is_err());
+}
+
+// tests
+
+#[test]
+fn downstream_bug1() {
+    let s: Styles = syn::parse_str(
+        "display: flex;
+        flex-direction: column;
+        flex-grow: 1;
+        flex-shrink: 0;",
+    )
+    .unwrap();
+    assert_eq!(
+        s.rules,
+        vec![
+            Style::Display(Display::Flex),
+            Style::FlexDirection(FlexDirection::Column),
+            Style::FlexGrow(1.0),
+            Style::FlexShrink(0.0)
+        ]
+    )
+}
+
+#[test]
+#[ignore]
+fn inline_logic() {
+    todo!()
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    /// Use if you want to test that something parses, but not if it looks the same when
+    /// stringified. Example "border: 1px" -> "border:1px" but we still might want to check that
+    /// the former parses.
+    fn parse(input: &str) -> Style {
+        syn::parse_str(input).unwrap()
+    }
+
+    /// This function can be used to quickly write tests to check that a parse and a stringify are
+    /// opposites.
+    fn round_trip_style(input: &str) {
+        assert_eq!(&parse(input).to_string(), input);
+    }
+
+    #[test]
+    fn border_bottom_left_radius() {
+        round_trip_style("border-bottom-left-radius:30% 3px");
+    }
+
+    #[test]
+    fn border_bottom_right_radius() {
+        round_trip_style("border-bottom-right-radius:0 0");
+    }
+
+    #[test]
+    fn border_collapse() {
+        round_trip_style("border-collapse:collapse");
+    }
+
+    #[test]
+    fn border_width() {
+        round_trip_style("border-width:1px");
+        round_trip_style("border-width:0 2px 50pt 0");
+    }
+}