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>
This commit is contained in:
@@ -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
|
||||
|
||||
+8
-1
@@ -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
|
||||
|
||||
@@ -15,7 +15,9 @@ allow = [
|
||||
"Apache-2.0",
|
||||
"Zlib",
|
||||
"ISC",
|
||||
"BSD-3-Clause"
|
||||
"BSD-3-Clause",
|
||||
"Unicode-3.0",
|
||||
"MPL-2.0",
|
||||
]
|
||||
|
||||
[bans]
|
||||
|
||||
@@ -23,7 +23,7 @@ class TrackSizingParser {
|
||||
}
|
||||
|
||||
parseSingleItem() {
|
||||
return this.parseItem();
|
||||
return this._parseItem();
|
||||
}
|
||||
|
||||
_parseItemList(separator, terminator = null) {
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`]?
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+34
-3
@@ -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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user