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
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;