gioser

tawasuyu/taffy

A high performance rust-powered UI layout library

Rust 501HTML 254C 19JavaScript 81412 Sutichaykuna

Add support for parsing Taffy style types from string (#929)

* Fix typo in test_helper script

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Bump MSRV to 1.71

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Implement `FromStr` for style types

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Expose parsing error messages as strings

* Allow Unicode-3.0 license

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Parse keyword enums using cssparser

---------

Signed-off-by: Nico Burns <nico@nicoburns.com>

70341c9f697b610470b0806a53266aa75343563b · ruwaq Nico Burns · (7 killa ñawpaq) · tayta be975378e · kay commitpi sach'ata qhaway

Semántico sayk'uy · 41

scripts/gentest/test_helper.js

src/style/available_space.rs

src/style/dimension.rs

src/style/grid.rs

src/util/mod.rs

src/util/parse.rs

15 qillqakuna · +614 −8

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 16ed2af39..ada98c14e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -20,13 +20,13 @@ jobs:
   # We only run `cargo build` (not `cargo test`) so as to avoid requiring dev-dependencies to build with the MSRV
   # version. Building is likely sufficient as runtime errors varying between rust versions is very unlikely.
   build-features-msrv:
-    name: "MSRV Build [Rust 1.65]"
+    name: "MSRV Build [Rust 1.71]"
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v6
       - uses: dtolnay/rust-toolchain@master
         with:
-          toolchain: 1.65
+          toolchain: 1.71
       - run: cargo build
 
   build-features-debug:
@@ -106,6 +106,15 @@ jobs:
       - run: cargo build --features serde
       - run: cargo test --tests --features serde
 
+  test-features-default-with-parse-faster:
+    name: "Test Suite [default + parse_faster]"
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v6
+      - uses: dtolnay/rust-toolchain@stable
+      - run: cargo build --features parse_faster
+      - run: cargo test --tests --features parse_faster
+
   test-features-default-except-content-size:
     name: "Test Suite [default except content_size]"
     runs-on: ubuntu-latest
diff --git a/Cargo.toml b/Cargo.toml
index 184af32aa..d863e6224 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,7 +7,7 @@ authors = [
     "Nico Burns <nico@nicoburns.com>",
 ]
 edition = "2021"
-rust-version = "1.65"
+rust-version = "1.71"
 include = ["src/**/*", "examples/**/*", "Cargo.toml", "README.md"]
 description = "A flexible UI layout library "
 repository = "https://github.com/DioxusLabs/taffy"
@@ -21,6 +21,7 @@ document-features = { version = "0.2.7", optional = true }
 serde = { version = "1.0", default-features = false, optional = true, features = ["serde_derive"] }
 slotmap = { version = "1.0.6", default-features = false, optional = true }
 grid = { version = "1.0.0", default-features = false, optional = true }
+cssparser = { version = "0.37.0", default-features = false, optional = true }
 
 [package.metadata.docs.rs]
 # To test all the documentation related features, run:
@@ -42,6 +43,8 @@ default = [
     "calc",
     "content_size",
     "detailed_layout_info",
+    "parse",
+    "parse_faster"
 ]
 #! ## Feature Flags
 #!
@@ -73,6 +76,10 @@ taffy_tree = ["dep:slotmap"]
 
 ## Add [`serde`] derives to Style structs
 serde = ["dep:serde"]
+## Implement `FromStr` trait for Taffy style types
+parse = ["dep:cssparser"]
+## Enable the `parse` feature with proc-macro dependent optimisations
+parse_faster = ["parse", "cssparser/fast_match_byte"]
 ## Allow Taffy to depend on the [`Rust Standard Library`](std)
 std = ["grid?/std", "serde?/std", "slotmap?/std"]
 ## Allow Taffy to depend on the alloc library
diff --git a/deny.toml b/deny.toml
index 430f2fac3..7b0b1b235 100644
--- a/deny.toml
+++ b/deny.toml
@@ -15,7 +15,9 @@ allow = [
     "Apache-2.0",
     "Zlib",
     "ISC",
-    "BSD-3-Clause"
+    "BSD-3-Clause",
+    "Unicode-3.0",
+    "MPL-2.0",
 ]
 
 [bans]
diff --git a/scripts/gentest/test_helper.js b/scripts/gentest/test_helper.js
index 6480f90d9..bc8c52c41 100644
--- a/scripts/gentest/test_helper.js
+++ b/scripts/gentest/test_helper.js
@@ -23,7 +23,7 @@ class TrackSizingParser {
   }
 
   parseSingleItem() {
-    return this.parseItem();
+    return this._parseItem();
   }
 
   _parseItemList(separator, terminator = null) {
diff --git a/src/lib.rs b/src/lib.rs
index 97d368009..51f6ba1df 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -123,6 +123,9 @@ pub use crate::tree::TaffyTree;
 #[doc(inline)]
 pub use crate::util::print_tree;
 
+#[cfg(feature = "parse")]
+pub use parse::{ParseError, ParseResult};
+
 pub use crate::compute::*;
 pub use crate::geometry::*;
 pub use crate::style::*;
diff --git a/src/style/alignment.rs b/src/style/alignment.rs
index f15e18cb9..81977e024 100644
--- a/src/style/alignment.rs
+++ b/src/style/alignment.rs
@@ -29,6 +29,18 @@ pub enum AlignItems {
     /// Stretch to fill the container
     Stretch,
 }
+
+#[cfg(feature = "parse")]
+crate::util::parse::impl_parse_for_keyword_enum!(AlignItems,
+    "start" => Start,
+    "end" => End,
+    "flex-start" => FlexStart,
+    "flex-end" => FlexEnd,
+    "center" => Center,
+    "baseline" => Baseline,
+    "stretch" => Stretch,
+);
+
 /// Used to control how child nodes are aligned.
 /// Does not apply to Flexbox, and will be ignored if specified on a flex container
 /// For Grid it controls alignment in the inline axis
@@ -89,6 +101,18 @@ pub enum AlignContent {
     SpaceAround,
 }
 
+#[cfg(feature = "parse")]
+crate::util::parse::impl_parse_for_keyword_enum!(AlignContent,
+    "start" => Start,
+    "end" => End,
+    "flex-start" => FlexStart,
+    "flex-end" => FlexEnd,
+    "center" => Center,
+    "stretch" => Stretch,
+    "space-between" => SpaceBetween,
+    "space-evenly" => SpaceEvenly,
+    "space-around" => SpaceAround,
+);
 /// Sets the distribution of space between and around content items
 /// For Flexbox it controls alignment in the main axis
 /// For Grid it controls alignment in the inline axis
diff --git a/src/style/available_space.rs b/src/style/available_space.rs
index 844037e0a..d2def420c 100644
--- a/src/style/available_space.rs
+++ b/src/style/available_space.rs
@@ -5,6 +5,9 @@ use crate::{
     Size,
 };
 
+#[cfg(feature = "parse")]
+use crate::util::parse::{from_str_from_css, parse_css_str_entirely, CssParseResult, FromCss, Parser, Token};
+
 /// The amount of space available to a node in a given axis
 /// <https://www.w3.org/TR/css-sizing-3/#available>
 #[derive(Copy, Clone, Debug, PartialEq)]
@@ -32,6 +35,21 @@ impl FromLength for AvailableSpace {
     }
 }
 
+#[cfg(feature = "parse")]
+impl FromCss for AvailableSpace {
+    fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> {
+        match parser.next()?.clone() {
+            Token::Number { value, .. } if value >= 0.0 => Ok(Self::Definite(value)),
+            Token::Dimension { value, .. } if value >= 0.0 => Ok(Self::Definite(value)),
+            Token::Ident(ident) if ident == "max-content" => Ok(Self::MaxContent),
+            Token::Ident(ident) if ident == "min-content" => Ok(Self::MinContent),
+            token => Err(parser.new_unexpected_token_error(token))?,
+        }
+    }
+}
+#[cfg(feature = "parse")]
+from_str_from_css!(AvailableSpace);
+
 impl AvailableSpace {
     /// Returns true for definite values, else false
     pub const fn is_definite(self) -> bool {
diff --git a/src/style/block.rs b/src/style/block.rs
index ae61931dd..467c2f761 100644
--- a/src/style/block.rs
+++ b/src/style/block.rs
@@ -47,3 +47,11 @@ pub enum TextAlign {
     /// Corresponds to `-webkit-center` or `-moz-center` in browsers
     LegacyCenter,
 }
+
+#[cfg(feature = "parse")]
+crate::util::parse::impl_parse_for_keyword_enum!(TextAlign,
+    "auto" => Auto,
+    "-webkit-left" => LegacyLeft,
+    "-webkit-right" => LegacyRight,
+    "-webkit-center" => LegacyCenter,
+);
diff --git a/src/style/dimension.rs b/src/style/dimension.rs
index b3b2f8797..1307b194f 100644
--- a/src/style/dimension.rs
+++ b/src/style/dimension.rs
@@ -2,6 +2,8 @@
 use super::CompactLength;
 use crate::geometry::Rect;
 use crate::style_helpers::{FromLength, FromPercent, TaffyAuto, TaffyZero};
+#[cfg(feature = "parse")]
+use crate::util::parse::{from_str_from_css, parse_css_str_entirely, CssParseResult, FromCss, Parser, Token};
 
 /// A unit of linear measurement
 ///
@@ -22,6 +24,20 @@ impl FromPercent for LengthPercentage {
         Self::percent(value.into())
     }
 }
+
+#[cfg(feature = "parse")]
+impl FromCss for LengthPercentage {
+    fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> {
+        match parser.next()?.clone() {
+            Token::Percentage { unit_value, .. } => Ok(Self::percent(unit_value)),
+            Token::Dimension { unit, value, .. } if unit == "px" => Ok(Self::length(value)),
+            token => Err(parser.new_unexpected_token_error(token))?,
+        }
+    }
+}
+#[cfg(feature = "parse")]
+from_str_from_css!(LengthPercentage);
+
 impl LengthPercentage {
     /// An absolute length in some abstract units. Users of Taffy may define what they correspond
     /// to in their application (pixels, logical pixels, mm, etc) as they see fit.
@@ -106,6 +122,20 @@ impl From<LengthPercentage> for LengthPercentageAuto {
     }
 }
 
+#[cfg(feature = "parse")]
+impl FromCss for LengthPercentageAuto {
+    fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> {
+        match parser.next()?.clone() {
+            Token::Percentage { unit_value, .. } => Ok(Self::percent(unit_value)),
+            Token::Dimension { unit, value, .. } if unit == "px" => Ok(Self::length(value)),
+            Token::Ident(ident) if ident == "auto" => Ok(Self::auto()),
+            token => Err(parser.new_unexpected_token_error(token))?,
+        }
+    }
+}
+#[cfg(feature = "parse")]
+from_str_from_css!(LengthPercentageAuto);
+
 impl LengthPercentageAuto {
     /// An absolute length in some abstract units. Users of Taffy may define what they correspond
     /// to in their application (pixels, logical pixels, mm, etc) as they see fit.
@@ -224,6 +254,20 @@ impl From<LengthPercentageAuto> for Dimension {
     }
 }
 
+#[cfg(feature = "parse")]
+impl FromCss for Dimension {
+    fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> {
+        match parser.next()?.clone() {
+            Token::Percentage { unit_value, .. } => Ok(Self::percent(unit_value)),
+            Token::Dimension { unit, value, .. } if unit == "px" => Ok(Self::length(value)),
+            Token::Ident(ident) if ident == "auto" => Ok(Self::auto()),
+            token => Err(parser.new_unexpected_token_error(token))?,
+        }
+    }
+}
+#[cfg(feature = "parse")]
+from_str_from_css!(Dimension);
+
 impl Dimension {
     /// An absolute length in some abstract units. Users of Taffy may define what they correspond
     /// to in their application (pixels, logical pixels, mm, etc) as they see fit.
diff --git a/src/style/flex.rs b/src/style/flex.rs
index 92b6e04b8..e4c7654ad 100644
--- a/src/style/flex.rs
+++ b/src/style/flex.rs
@@ -85,6 +85,13 @@ pub enum FlexWrap {
     WrapReverse,
 }
 
+#[cfg(feature = "parse")]
+crate::util::parse::impl_parse_for_keyword_enum!(FlexWrap,
+    "nowrap" => NoWrap,
+    "wrap" => Wrap,
+    "wrap-reverse" => WrapReverse,
+);
+
 /// The direction of the flexbox layout main axis.
 ///
 /// There are always two perpendicular layout axes: main (or primary) and cross (or secondary).
@@ -118,6 +125,14 @@ pub enum FlexDirection {
     ColumnReverse,
 }
 
+#[cfg(feature = "parse")]
+crate::util::parse::impl_parse_for_keyword_enum!(FlexDirection,
+    "row" => Row,
+    "column" => Column,
+    "row-reverse" => RowReverse,
+    "column-reverse" => ColumnReverse,
+);
+
 impl FlexDirection {
     #[inline]
     /// Is the direction [`FlexDirection::Row`] or [`FlexDirection::RowReverse`]?
diff --git a/src/style/float.rs b/src/style/float.rs
index 049717a22..6a8fb51b9 100644
--- a/src/style/float.rs
+++ b/src/style/float.rs
@@ -16,6 +16,13 @@ pub enum Float {
     None,
 }
 
+#[cfg(feature = "parse")]
+crate::util::parse::impl_parse_for_keyword_enum!(Float,
+    "left" => Left,
+    "right" => Right,
+    "none" => None,
+);
+
 /// Whether a box that is definitely floated is floated to the left
 /// of to the right.
 ///
@@ -63,3 +70,11 @@ pub enum Clear {
     #[default]
     None,
 }
+
+#[cfg(feature = "parse")]
+crate::util::parse::impl_parse_for_keyword_enum!(Clear,
+    "left" => Left,
+    "right" => Right,
+    "both" => Both,
+    "none" => None,
+);
diff --git a/src/style/grid.rs b/src/style/grid.rs
index 5aedf94f7..c8598ebe1 100644
--- a/src/style/grid.rs
+++ b/src/style/grid.rs
@@ -10,6 +10,11 @@ use crate::sys::{DefaultCheapStr, Vec};
 use core::cmp::{max, min};
 use core::fmt::Debug;
 
+#[cfg(feature = "parse")]
+use crate::util::parse::{
+    from_str_from_css, parse_css_str_entirely, CssParseResult, FromCss, ParseError, Parser, Token,
+};
+
 /// Defines a grid area
 #[derive(Debug, Clone, PartialEq)]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
@@ -287,6 +292,29 @@ pub enum GridAutoFlow {
     ColumnDense,
 }
 
+#[cfg(feature = "parse")]
+impl core::str::FromStr for GridAutoFlow {
+    type Err = ();
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let s = s.trim();
+        // TODO: check for space between keywords
+        let is_dense = s.contains("dense");
+        if s.starts_with("row") {
+            return match is_dense {
+                true => Ok(Self::RowDense),
+                false => Ok(Self::Row),
+            };
+        }
+        if s.starts_with("column") {
+            return match is_dense {
+                true => Ok(Self::ColumnDense),
+                false => Ok(Self::Column),
+            };
+        }
+        Err(())
+    }
+}
+
 impl GridAutoFlow {
     /// Whether grid auto placement uses the sparse placement algorithm or the dense placement algorithm
     /// See: <https://developer.mozilla.org/en-US/docs/Web/CSS/grid-auto-flow#values>
@@ -378,6 +406,68 @@ impl<S: CheapCloneStr> TaffyGridSpan for Line<GridPlacement<S>> {
     }
 }
 
+#[cfg(feature = "parse")]
+impl<S: CheapCloneStr> FromCss for GridPlacement<S> {
+    fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> {
+        let mut span = false;
+        let mut number = None;
+        let mut ident = None;
+
+        while !parser.is_exhausted() {
+            let token = parser.next()?.clone();
+            match &token {
+                Token::Ident(s) => match s.as_ref() {
+                    "auto" => {
+                        if span || number.is_some() || ident.is_some() {
+                            return Err(parser.new_unexpected_token_error(token));
+                        }
+                        parser.expect_exhausted()?;
+                        return Ok(Self::Auto);
+                    }
+                    "span" => {
+                        if span {
+                            return Err(parser.new_unexpected_token_error(token));
+                        }
+                        span = true;
+                    }
+                    other => {
+                        if ident.is_some() {
+                            return Err(parser.new_unexpected_token_error(token));
+                        }
+                        ident = Some(S::from(other));
+                    }
+                },
+                Token::Number { int_value: Some(value), .. } if *value != 0 => {
+                    if number.is_some() {
+                        return Err(parser.new_unexpected_token_error(token));
+                    }
+                    number = Some(*value);
+                }
+                _ => return Err(parser.new_unexpected_token_error(token)),
+            };
+        }
+
+        match (span, number, ident) {
+            (true, None, None) => Ok(Self::Span(0)),
+            (true, Some(number), None) => Ok(Self::Span(number as u16)),
+            (true, None, Some(ident)) => Ok(Self::NamedSpan(ident, 0)),
+            (true, Some(number), Some(ident)) => Ok(Self::NamedSpan(ident, number as u16)),
+            (false, Some(number), None) => Ok(Self::Line(GridLine::from(number as i16))),
+            (false, Some(number), Some(ident)) => Ok(Self::NamedLine(ident, number as i16)),
+            (false, None, Some(ident)) => Ok(Self::NamedLine(ident, 0)),
+            (false, None, None) => Err(parser.new_error(cssparser::BasicParseErrorKind::EndOfInput)),
+        }
+    }
+}
+
+#[cfg(feature = "parse")]
+impl<S: CheapCloneStr> core::str::FromStr for GridPlacement<S> {
+    type Err = ParseError;
+    fn from_str(input: &str) -> Result<Self, Self::Err> {
+        parse_css_str_entirely(input)
+    }
+}
+
 impl<S: CheapCloneStr> GridPlacement<S> {
     /// Apply a mapping function if the [`GridPlacement`] is a `Line`. Otherwise return `self` unmodified.
     pub fn into_origin_zero_placement_ignoring_named(&self, explicit_track_count: u16) -> OriginZeroGridPlacement {
@@ -619,6 +709,37 @@ impl From<MinTrackSizingFunction> for MaxTrackSizingFunction {
         Self(input.0)
     }
 }
+
+#[cfg(feature = "parse")]
+impl FromCss for MaxTrackSizingFunction {
+    fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> {
+        let token = parser.next()?.clone();
+        match token {
+            Token::Percentage { unit_value, .. } => Ok(Self::percent(unit_value)),
+            Token::Dimension { unit, value, .. } if unit == "px" => Ok(Self::length(value)),
+            Token::Dimension { unit, value, .. } if unit == "fr" && value.is_sign_positive() => Ok(Self::fr(value)),
+            Token::Ident(ref ident) => match ident.as_ref() {
+                "auto" => Ok(Self::auto()),
+                "min-content" => Ok(Self::min_content()),
+                "max-content" => Ok(Self::max_content()),
+                _ => Err(parser.new_unexpected_token_error(token))?,
+            },
+            Token::Function(ref name) if name.as_ref() == "fit-content" => parser.parse_nested_block(|parser| {
+                let token = parser.next()?.clone();
+                match token {
+                    Token::Percentage { unit_value, .. } => Ok(Self::fit_content_percent(unit_value)),
+                    Token::Dimension { unit, value, .. } if unit == "px" => Ok(Self::fit_content_px(value)),
+                    token => Err(parser.new_unexpected_token_error(token))?,
+                }
+            }),
+            token => Err(parser.new_unexpected_token_error(token))?,
+        }
+    }
+}
+
+#[cfg(feature = "parse")]
+from_str_from_css!(MaxTrackSizingFunction);
+
 #[cfg(feature = "serde")]
 impl<'de> serde::Deserialize<'de> for MaxTrackSizingFunction {
     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
@@ -903,6 +1024,37 @@ impl From<Dimension> for MinTrackSizingFunction {
         Self(input.0)
     }
 }
+
+impl From<MaxTrackSizingFunction> for MinTrackSizingFunction {
+    fn from(input: MaxTrackSizingFunction) -> Self {
+        if input.is_fr() || input.is_fit_content() {
+            return Self::auto();
+        }
+        Self(input.0)
+    }
+}
+
+#[cfg(feature = "parse")]
+impl FromCss for MinTrackSizingFunction {
+    fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> {
+        let token = parser.next()?.clone();
+        match token {
+            Token::Percentage { unit_value, .. } => Ok(Self::percent(unit_value)),
+            Token::Dimension { unit, value, .. } if unit == "px" => Ok(Self::length(value)),
+            Token::Ident(ref ident) => match ident.as_ref() {
+                "auto" => Ok(Self::auto()),
+                "min-content" => Ok(Self::min_content()),
+                "max-content" => Ok(Self::max_content()),
+                _ => Err(parser.new_unexpected_token_error(token))?,
+            },
+            token => Err(parser.new_unexpected_token_error(token))?,
+        }
+    }
+}
+
+#[cfg(feature = "parse")]
+from_str_from_css!(MinTrackSizingFunction);
+
 #[cfg(feature = "serde")]
 impl<'de> serde::Deserialize<'de> for MinTrackSizingFunction {
     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
@@ -1136,6 +1288,33 @@ impl From<Dimension> for TrackSizingFunction {
     }
 }
 
+#[cfg(feature = "parse")]
+impl FromCss for TrackSizingFunction {
+    fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> {
+        // Try to parse a minmax() function
+        if let Ok(value) = parser.try_parse(|parser| {
+            parser.expect_function_matching("minmax")?;
+            parser.parse_nested_block(|parser| {
+                let min = MinTrackSizingFunction::from_css(parser)?;
+                parser.expect_comma()?;
+                let max = MaxTrackSizingFunction::from_css(parser)?;
+
+                Ok(Self { min, max })
+            })
+        }) {
+            return Ok(value);
+        }
+
+        // Else parse a max track sizing function
+        let max = MaxTrackSizingFunction::from_css(parser)?;
+        let min = max.into();
+        Ok(Self { min, max })
+    }
+}
+
+#[cfg(feature = "parse")]
+from_str_from_css!(TrackSizingFunction);
+
 /// The first argument to a repeated track definition. This type represents the type of automatic repetition to perform.
 ///
 /// See <https://www.w3.org/TR/css-grid-1/#auto-repeat> for an explanation of how auto-repeated track definitions work
@@ -1180,6 +1359,20 @@ impl TryFrom<&str> for RepetitionCount {
     }
 }
 
+#[cfg(feature = "parse")]
+impl FromCss for RepetitionCount {
+    fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> {
+        match parser.next()?.clone() {
+            Token::Number { int_value: Some(value), .. } if value.is_positive() => Ok(Self::Count(value as _)),
+            Token::Ident(ident) if ident == "auto-fit" => Ok(Self::AutoFit),
+            Token::Ident(ident) if ident == "auto-fill" => Ok(Self::AutoFill),
+            token => Err(parser.new_unexpected_token_error(token))?,
+        }
+    }
+}
+#[cfg(feature = "parse")]
+from_str_from_css!(RepetitionCount);
+
 /// A typed representation of a `repeat(..)` in `grid-template-*` value
 #[derive(Clone, PartialEq, Debug)]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
@@ -1285,3 +1478,117 @@ impl<S: CheapCloneStr> From<MinMax<MinTrackSizingFunction, MaxTrackSizingFunctio
         Self::Single(input)
     }
 }
+
+#[cfg(feature = "parse")]
+impl<S: CheapCloneStr> FromCss for GridTemplateComponent<S> {
+    fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> {
+        // Try to parse a minmax() function
+        if let Ok(value) = parser.try_parse(|parser| {
+            parser.expect_function_matching("repeat")?;
+            parser.parse_nested_block(|parser| {
+                let count = RepetitionCount::from_css(parser)?;
+                parser.expect_comma()?;
+                let tracks = GridTemplateTracks::<S, TrackSizingFunction>::from_css(parser)?;
+
+                Ok(Self::Repeat(GridTemplateRepetition { count, tracks: tracks.tracks, line_names: tracks.line_names }))
+            })
+        }) {
+            return Ok(value);
+        }
+
+        // Else parse a track sizing function
+        let track_sizing_function = TrackSizingFunction::from_css(parser)?;
+        Ok(Self::Single(track_sizing_function))
+    }
+}
+#[cfg(feature = "parse")]
+impl<S: CheapCloneStr> core::str::FromStr for GridTemplateComponent<S> {
+    type Err = ParseError;
+    fn from_str(input: &str) -> Result<Self, Self::Err> {
+        parse_css_str_entirely(input)
+    }
+}
+
+#[derive(Clone, PartialEq, Debug)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[doc(hidden)]
+pub struct GridTemplateTracks<S: CheapCloneStr, Track> {
+    /// The tracks to repeat
+    pub tracks: Vec<Track>,
+    /// The line names for the repeated tracks
+    pub line_names: Vec<Vec<S>>,
+}
+
+impl<S: CheapCloneStr, Track> Default for GridTemplateTracks<S, Track> {
+    fn default() -> Self {
+        Self { tracks: Vec::new(), line_names: Vec::new() }
+    }
+}
+
+#[cfg(feature = "parse")]
+impl<S: CheapCloneStr, Track: FromCss + Debug> FromCss for GridTemplateTracks<S, Track> {
+    fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> {
+        fn try_parse_line_names<'i, S: CheapCloneStr>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Vec<S>> {
+            parser.try_parse(|parser| {
+                parser.expect_square_bracket_block()?;
+                parser.parse_nested_block(|parser| {
+                    let mut line_names = Vec::new();
+                    while !parser.is_exhausted() {
+                        line_names.push(S::from(parser.expect_ident_cloned()?.as_ref()));
+                    }
+                    Ok(line_names)
+                })
+            })
+        }
+
+        let mut tracks = Self::default();
+        if let Ok(line_names) = try_parse_line_names(parser) {
+            tracks.line_names.push(line_names);
+        }
+
+        while !parser.is_exhausted() {
+            tracks.tracks.push(Track::from_css(parser)?);
+            if let Ok(line_names) = try_parse_line_names(parser) {
+                tracks.line_names.push(line_names);
+            }
+        }
+
+        if tracks.tracks.is_empty() {
+            return Err(parser.new_error(cssparser::BasicParseErrorKind::EndOfInput));
+        }
+
+        Ok(tracks)
+    }
+}
+#[cfg(feature = "parse")]
+impl<S: CheapCloneStr, Track: FromCss + Debug> core::str::FromStr for GridTemplateTracks<S, Track> {
+    type Err = ParseError;
+    fn from_str(input: &str) -> Result<Self, Self::Err> {
+        parse_css_str_entirely(input)
+    }
+}
+
+#[derive(Default)]
+#[doc(hidden)]
+pub struct GridAutoTracks(pub Vec<TrackSizingFunction>);
+
+#[cfg(feature = "parse")]
+impl FromCss for GridAutoTracks {
+    fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> {
+        let mut tracks = Self::default();
+        while !parser.is_exhausted() {
+            tracks.0.push(TrackSizingFunction::from_css(parser)?);
+        }
+        if tracks.0.is_empty() {
+            return Err(parser.new_error(cssparser::BasicParseErrorKind::EndOfInput));
+        }
+        Ok(tracks)
+    }
+}
+#[cfg(feature = "parse")]
+impl core::str::FromStr for GridAutoTracks {
+    type Err = ParseError;
+    fn from_str(input: &str) -> Result<Self, Self::Err> {
+        parse_css_str_entirely(input)
+    }
+}
diff --git a/src/style/mod.rs b/src/style/mod.rs
index fe60e0411..2b6ed9103 100644
--- a/src/style/mod.rs
+++ b/src/style/mod.rs
@@ -27,9 +27,9 @@ pub use self::flex::{FlexDirection, FlexWrap, FlexboxContainerStyle, FlexboxItem
 pub use self::float::{Clear, Float, FloatDirection};
 #[cfg(feature = "grid")]
 pub use self::grid::{
-    GenericGridPlacement, GenericGridTemplateComponent, GenericRepetition, GridAutoFlow, GridContainerStyle,
-    GridItemStyle, GridPlacement, GridTemplateComponent, GridTemplateRepetition, MaxTrackSizingFunction,
-    MinTrackSizingFunction, RepetitionCount, TrackSizingFunction,
+    GenericGridPlacement, GenericGridTemplateComponent, GenericRepetition, GridAutoFlow, GridAutoTracks,
+    GridContainerStyle, GridItemStyle, GridPlacement, GridTemplateComponent, GridTemplateRepetition,
+    GridTemplateTracks, MaxTrackSizingFunction, MinTrackSizingFunction, RepetitionCount, TrackSizingFunction,
 };
 #[cfg(feature = "grid")]
 pub(crate) use self::grid::{GridAreaAxis, GridAreaEnd};
@@ -210,6 +210,17 @@ impl Default for Display {
     }
 }
 
+#[cfg(feature = "parse")]
+crate::util::parse::impl_parse_for_keyword_enum!(Display,
+    "none" => None,
+    #[cfg(feature = "flexbox")]
+    "flex" => Flex,
+    #[cfg(feature = "grid")]
+    "grid" => Grid,
+    #[cfg(feature = "block_layout")]
+    "block" => Block,
+);
+
 impl core::fmt::Display for Display {
     fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
         match self {
@@ -270,6 +281,12 @@ pub enum Position {
     Absolute,
 }
 
+#[cfg(feature = "parse")]
+crate::util::parse::impl_parse_for_keyword_enum!(Position,
+    "relative" => Relative,
+    "absolute" => Absolute,
+);
+
 /// Specifies whether size styles for this node are assigned to the node's "content box" or "border box"
 ///
 /// - The "content box" is the node's inner size excluding padding, border and margin
@@ -293,6 +310,12 @@ pub enum BoxSizing {
     ContentBox,
 }
 
+#[cfg(feature = "parse")]
+crate::util::parse::impl_parse_for_keyword_enum!(BoxSizing,
+    "border-box" => BorderBox,
+    "content-box" => ContentBox,
+);
+
 /// How children overflowing their container should affect layout
 ///
 /// In CSS the primary effect of this property is to control whether contents of a parent container that overflow that container should
@@ -347,6 +370,14 @@ impl Overflow {
     }
 }
 
+#[cfg(feature = "parse")]
+crate::util::parse::impl_parse_for_keyword_enum!(Overflow,
+    "visible" => Visible,
+    "hidden" => Hidden,
+    "clip" => Clip,
+    "scroll" => Scroll,
+);
+
 /// A typed representation of the CSS style information for a single node.
 ///
 /// The most important idea in flexbox is the notion of a "main" and "cross" axis, which are always perpendicular to each other.
diff --git a/src/util/mod.rs b/src/util/mod.rs
index 00f3a8fe0..0201f380a 100644
--- a/src/util/mod.rs
+++ b/src/util/mod.rs
@@ -17,6 +17,11 @@ pub use print::print_tree;
 #[cfg(feature = "std")]
 pub use print::write_tree;
 
+#[cfg(feature = "parse")]
+pub(crate) mod parse;
+#[cfg(feature = "parse")]
+pub use parse::{ParseError, ParseResult};
+
 /// Deserialize a type `S` by deserializing a string, then using the `FromStr`
 /// impl of `S` to create the result. The generic type `S` is not required to
 /// implement `Deserialize`.
diff --git a/src/util/parse.rs b/src/util/parse.rs
new file mode 100644
index 000000000..71dd249f6
--- /dev/null
+++ b/src/util/parse.rs
@@ -0,0 +1,118 @@
+//! Helpers for parsing style types from strings
+
+use cssparser::BasicParseError;
+pub(crate) use cssparser::{Parser, ParserInput, Token};
+use std::borrow::Cow;
+
+/// Error type for parsing a type from `cssparser::Parser`
+pub(crate) type CssParseError<'i> = cssparser::ParseError<'i, Cow<'i, str>>;
+
+/// Result type for parsing a type from `cssparser::Parser`
+pub(crate) type CssParseResult<'i, T> = Result<T, CssParseError<'i>>;
+
+/// Error type for parsing a type from string
+#[derive(Clone, Debug)]
+pub struct ParseError(String);
+
+impl core::fmt::Display for ParseError {
+    fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        write!(fmt, "{}", &self.0)
+    }
+}
+impl<'i> From<CssParseError<'i>> for ParseError {
+    fn from(value: CssParseError<'i>) -> Self {
+        Self(value.to_string())
+    }
+}
+impl<'i> From<BasicParseError<'i>> for ParseError {
+    fn from(value: BasicParseError<'i>) -> Self {
+        Self(CssParseError::from(value).to_string())
+    }
+}
+#[cfg(feature = "std")]
+impl std::error::Error for ParseError {}
+
+/// Result type for parsing a type from string
+pub type ParseResult<T> = Result<T, ParseError>;
+
+/// Trait for parsing a type from a `cssparser::Parser` input
+pub(crate) trait FromCss: Sized {
+    /// Parse type from a `cssparser::Parser` input
+    fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self>;
+}
+
+/// Parse a string into a type that implements `FromCss`, ensuring that the
+/// entire input string is consumed.
+pub(crate) fn parse_css_str_entirely<T: FromCss>(input: &str) -> Result<T, ParseError> {
+    let mut parser_input = ParserInput::new(input);
+    let mut parser = Parser::new(&mut parser_input);
+    parser.parse_entirely(|parser| T::from_css(parser)).map_err(|err| ParseError(err.to_string()))
+}
+
+/// Automatically implement `FromStr` for a type that already implemented `FromCss`
+macro_rules! from_str_from_css {
+    ($ty:ident) => {
+        #[cfg(feature = "parse")]
+        impl core::str::FromStr for $ty {
+            type Err = $crate::ParseError;
+            fn from_str(input: &str) -> Result<Self, Self::Err> {
+                parse_css_str_entirely(input)
+            }
+        }
+    };
+}
+pub(crate) use from_str_from_css;
+
+/// Implements the `FromCss` and `FromStr` traits for a simple enum that consists of just keywords
+macro_rules! impl_parse_for_keyword_enum {
+    (
+        // The type name (followed by a comma)
+        $ty:ident,
+
+        // Repeat 1-or-more times
+        $(
+            // 0-or-more metadata attributes (e.g. #[cfg] attributes for conditional compilation)
+            $( #[$meta: meta] )*
+
+            // keyword => enum_variant (e.g. "center" => AlignItems::Center)
+            $keyword:literal => $enum_variant:ident,
+        )+
+    ) => {
+
+        // Impl FromCss for the type
+        impl $crate::util::parse::FromCss for $ty {
+            fn from_css<'i>(input: &mut cssparser::Parser<'i, '_>) -> $crate::util::parse::CssParseResult<'i, Self> {
+
+                // Parse an ident (or return an error)
+                let ident = input.expect_ident()?;
+
+                // Match on the ident (case insensitive)
+                cssparser::match_ignore_ascii_case! { &*ident,
+
+                    // Define each item in the macro definition as a match case
+                    $(
+                        // Add the metadata attributes to the match case
+                        $( #[$meta] )*
+                        // If the keyword matches, return the corresponding variant of the enum
+                        $keyword => return Ok(Self::$enum_variant),
+                    )+
+
+                    // If none of the cases match the input, return an "unexpected token" error
+                    _ => {
+                        let ident = ident.clone();
+                        Err(input.new_unexpected_token_error(cssparser::Token::Ident(ident)))
+                    }
+                }
+            }
+        }
+
+        // Impl FromStr for the type
+        impl core::str::FromStr for $ty {
+            type Err = $crate::ParseError;
+            fn from_str(input: &str) -> Result<Self, Self::Err> {
+                $crate::util::parse::parse_css_str_entirely(input)
+            }
+        }
+    };
+}
+pub(crate) use impl_parse_for_keyword_enum;