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 ", ] 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 /// #[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 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 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 { + 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: @@ -378,6 +406,68 @@ impl TaffyGridSpan for Line> { } } +#[cfg(feature = "parse")] +impl FromCss for GridPlacement { + 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 core::str::FromStr for GridPlacement { + type Err = ParseError; + fn from_str(input: &str) -> Result { + parse_css_str_entirely(input) + } +} + impl GridPlacement { /// 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 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(deserializer: D) -> Result @@ -903,6 +1024,37 @@ impl From for MinTrackSizingFunction { Self(input.0) } } + +impl From 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(deserializer: D) -> Result @@ -1136,6 +1288,33 @@ impl From 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 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 From FromCss for GridTemplateComponent { + 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::::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 core::str::FromStr for GridTemplateComponent { + type Err = ParseError; + fn from_str(input: &str) -> Result { + parse_css_str_entirely(input) + } +} + +#[derive(Clone, PartialEq, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[doc(hidden)] +pub struct GridTemplateTracks { + /// The tracks to repeat + pub tracks: Vec, + /// The line names for the repeated tracks + pub line_names: Vec>, +} + +impl Default for GridTemplateTracks { + fn default() -> Self { + Self { tracks: Vec::new(), line_names: Vec::new() } + } +} + +#[cfg(feature = "parse")] +impl FromCss for GridTemplateTracks { + 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> { + 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 core::str::FromStr for GridTemplateTracks { + type Err = ParseError; + fn from_str(input: &str) -> Result { + parse_css_str_entirely(input) + } +} + +#[derive(Default)] +#[doc(hidden)] +pub struct GridAutoTracks(pub Vec); + +#[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 { + 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>; + +/// 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> for ParseError { + fn from(value: CssParseError<'i>) -> Self { + Self(value.to_string()) + } +} +impl<'i> From> 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 = Result; + +/// 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(input: &str) -> Result { + 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 { + 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 { + $crate::util::parse::parse_css_str_entirely(input) + } + } + }; +} +pub(crate) use impl_parse_for_keyword_enum;