diff --git a/Cargo.toml b/Cargo.toml index 1e648cfa3..0b289f416 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ license = "MIT" [dependencies] arrayvec = { version = "0.7", default-features = false } +cssparser = { version = "0.31.0", optional = true } num-traits = { version = "0.2", default-features = false } serde = { version = "1.0", optional = true, features = ["serde_derive"] } slotmap = { version = "1.0.6", optional = true } @@ -22,6 +23,7 @@ grid = { version = "0.9.0", optional = true } [features] default = ["std", "flexbox", "grid", "taffy_tree"] +css-syntax = ["dep:cssparser"] flexbox = [] grid = ["alloc", "dep:grid"] alloc = [] diff --git a/src/style/css_syntax/mod.rs b/src/style/css_syntax/mod.rs new file mode 100644 index 000000000..44cf79c59 --- /dev/null +++ b/src/style/css_syntax/mod.rs @@ -0,0 +1,289 @@ +use core::fmt; + +use crate::style::css_syntax::values::non_negative; +use crate::style::css_syntax::values::MaybeAuto; +use crate::style::Style; +use crate::Taffy; +use cssparser::match_ignore_ascii_case; +use cssparser::BasicParseErrorKind; +use cssparser::CowRcStr; +use cssparser::DeclarationListParser; +use cssparser::Parser; +use cssparser::ParserInput; +use cssparser::SourceLocation; +use cssparser::Token; + +mod values; + +#[derive(Debug, Clone)] +pub struct CssParseError<'input> { + location: SourceLocation, + declaration_source: &'input str, + kind: ParseErrorKind<'input>, +} + +#[derive(Debug, Clone)] +enum ParseErrorKind<'i> { + InvalidOrUnknownProperty, + InvalidOrUnknownKeyword(CowRcStr<'i>), + UnexpectedToken(Token<'i>), + UnexpectedEndOfInput, + NegativeValue, +} + +type ParseError<'i> = cssparser::ParseError<'i, ParseErrorKind<'i>>; + +impl fmt::Display for CssParseError<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let SourceLocation { line, column } = self.location; + write!(f, "{} at {line}:{column}: `{}`", self.kind, self.declaration_source) + } +} + +impl fmt::Display for ParseErrorKind<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidOrUnknownProperty => write!(f, "invalid or unknown property"), + Self::InvalidOrUnknownKeyword(keyword) => write!(f, "invalid or unknown keyword: `{keyword}`"), + Self::UnexpectedEndOfInput => write!(f, "unexpected end of input"), + Self::UnexpectedToken(token) => write!(f, "unexpected token {token:?}"), + Self::NegativeValue => write!(f, "value must be positive or zero"), + } + } +} + +impl<'i> From> for ParseErrorKind<'i> { + fn from(kind: BasicParseErrorKind<'i>) -> Self { + match kind { + BasicParseErrorKind::UnexpectedToken(token) => Self::UnexpectedToken(token), + BasicParseErrorKind::EndOfInput => Self::UnexpectedEndOfInput, + BasicParseErrorKind::AtRuleInvalid(_) + | BasicParseErrorKind::AtRuleBodyInvalid + | BasicParseErrorKind::QualifiedRuleInvalid => unreachable!(), + } + } +} + +impl Taffy { + /// Parse [`Style`] from a list declarations in CSS syntax. + /// + /// Parsing is infallible. + /// Errors such as invalid syntax or unknown/unsupported property or value + /// are logged in the returned `Vec` and cause the current declaration to be ignored. + /// Unspecified properties get their initial values from [`Style::DEFAULT`]. + /// + /// Requires the `css-syntax` Cargo feature to be enabled. + pub fn parse_css_style<'input>(&self, css: &'input str) -> (Style, Vec>) { + let mut errors = Vec::new(); + let mut input = ParserInput::new(css); + let mut parser = Parser::new(&mut input); + let declaration_parser = DeclarationParser { style: Style::DEFAULT, taffy: self }; + let mut iter = DeclarationListParser::new(&mut parser, declaration_parser); + for result in &mut iter { + match result { + Ok(()) => {} + Err((error, declaration_source)) => errors.push(CssParseError { + location: error.location, + declaration_source, + kind: match error.kind { + cssparser::ParseErrorKind::Basic(kind) => kind.into(), + cssparser::ParseErrorKind::Custom(kind) => kind, + }, + }), + } + } + (iter.parser.style, errors) + } + + /// Serialize the given style to CSS syntax + pub fn style_to_css(&self, style: &Style) -> String { + // Cause a compiler error or warning if we never set a struct field during parsing + let Style { + display, + overflow, + position, + inset, + size, + min_size, + max_size, + aspect_ratio, + margin, + padding, + border, + align_items, + align_self, + justify_items, + justify_self, + align_content, + justify_content, + gap, + flex_direction, + flex_wrap, + flex_basis, + flex_grow, + flex_shrink, + grid_template_rows, + grid_template_columns, + grid_auto_rows, + grid_auto_columns, + grid_auto_flow, + grid_row, + grid_column, + } = style; + + let mut css = String::new(); + + // This is a macro because closures can’t take a generic parameter + // and `fn` items can’t capture local variables. + macro_rules! decl { + ($name: expr, $value: expr) => { + serialize_one_declaration(self, &mut css, $name, $value) + }; + } + + // TODO: deal with shorthand v.s. longhand properties per + // https://drafts.csswg.org/cssom/#serialize-a-css-declaration-block + if *display != Style::DEFAULT.display { + decl!("display", display); + } + todo!() + } +} + +fn serialize_one_declaration(taffy: &Taffy, dest: &mut String, name: &str, value: &impl CssValue) { + if !dest.is_empty() { + dest.push_str("; ") + } + // `unwrap` should never panic since `impl fmt::Write for String` never returns `Err` + cssparser::serialize_identifier(name, dest).unwrap(); + dest.push_str(": "); + value.serialize(dest, taffy); +} + +struct DeclarationParser<'taffy> { + style: Style, + taffy: &'taffy Taffy, +} + +impl<'i> cssparser::DeclarationParser<'i> for DeclarationParser<'_> { + // Instead of a data structure to return a single parsed declaration + // we mutate `self.style` in place. + type Declaration = (); + + type Error = ParseErrorKind<'i>; + + fn parse_value<'t>( + &mut self, + property_name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<(), ParseError<'i>> { + // Cause a compiler error or warning if we never set a struct field during parsing + let Style { + display, + overflow, + position, + inset, + size, + min_size, + max_size, + aspect_ratio, + margin, + padding, + border, + align_items, + align_self, + justify_items, + justify_self, + align_content, + justify_content, + gap, + flex_direction, + flex_wrap, + flex_basis, + flex_grow, + flex_shrink, + grid_template_rows, + grid_template_columns, + grid_auto_rows, + grid_auto_columns, + grid_auto_flow, + grid_row, + grid_column, + } = &mut self.style; + + // This is a macro because closures can’t have a generic return type + // and `fn` items can’t capture local variables. + macro_rules! parse { + () => { + input.parse_entirely(|input| CssValue::parse(input, &self.taffy))? + }; + } + + match_ignore_ascii_case! { &*property_name, + // https://drafts.csswg.org/css2/#display-prop + // https://drafts.csswg.org/css-flexbox/#flex-containers + // https://drafts.csswg.org/css-grid/#grid-containers + "display" => *display = parse!(), + // https://w3c.github.io/csswg-drafts/css-overflow/#propdef-overflow + "overflow" => *overflow = parse!(), + "overflow-x" => overflow.x = parse!(), + "overflow-y" => overflow.y = parse!(), + // https://w3c.github.io/csswg-drafts/css-position-3/#position-property + "position" => *position = parse!(), + // https://w3c.github.io/csswg-drafts/css-position-3/#inset-shorthands + "inset" => *inset = parse!(), + "top" => inset.top = parse!(), + "right" => inset.right = parse!(), + "bottom" => inset.bottom = parse!(), + "left" => inset.left = parse!(), + // https://drafts.csswg.org/css-sizing/#preferred-size-properties + "width" => size.width = non_negative(parse!()), + "height" => size.height = non_negative(parse!()), + // https://drafts.csswg.org/css-sizing/#min-size-properties + "min-width" => min_size.width = non_negative(parse!()), + "min-height" => min_size.height = non_negative(parse!()), + // https://drafts.csswg.org/css-sizing/#max-size-properties + "max-width" => max_size.width = non_negative(parse!()), + "max-height" => max_size.height = non_negative(parse!()), + // https://w3c.github.io/csswg-drafts/css-sizing-4/#aspect-ratio + "aspect-ratio" => *aspect_ratio = MaybeAuto::to_opt_f32(parse!()), + // https://drafts.csswg.org/css2/#margin-properties + "margin" => *margin = parse!(), + "margin-top" => margin.top = parse!(), + "margin-right" => margin.right = parse!(), + "margin-bottom" => margin.bottom = parse!(), + "margin-left" => margin.left = parse!(), + // https://drafts.csswg.org/css2/#padding-properties + "padding" => *padding = non_negative(parse!()), + "padding-top" => padding.top = non_negative(parse!()), + "padding-right" => padding.right = non_negative(parse!()), + "padding-bottom" => padding.bottom = non_negative(parse!()), + "padding-left" => padding.left = non_negative(parse!()), + + _ => { + return Err(input.new_custom_error(ParseErrorKind::InvalidOrUnknownProperty)) + } + } + Ok(()) + } +} + +// Default methods always return Err for no supported at-rule +impl<'i> cssparser::AtRuleParser<'i> for DeclarationParser<'_> { + type Prelude = (); + type AtRule = (); + type Error = ParseErrorKind<'i>; +} + +// Default methods always return Err for no supported nested qualified rule +impl<'i> cssparser::QualifiedRuleParser<'i> for DeclarationParser<'_> { + type Prelude = (); + type QualifiedRule = (); + type Error = ParseErrorKind<'i>; +} + +trait CssValue: Sized { + fn parse<'i, 't>(input: &mut Parser<'i, 't>, taffy: &Taffy) -> Result>; + + fn serialize(&self, dest: &mut String, taffy: &Taffy); +} diff --git a/src/style/css_syntax/values.rs b/src/style/css_syntax/values.rs new file mode 100644 index 000000000..3dc3269bc --- /dev/null +++ b/src/style/css_syntax/values.rs @@ -0,0 +1,333 @@ +use core::fmt::Write; + +use crate::geometry::Point; +use crate::geometry::Rect; +use crate::style::css_syntax::match_ignore_ascii_case; +use crate::style::css_syntax::CssValue; +use crate::style::css_syntax::ParseError; +use crate::style::css_syntax::ParseErrorKind; +use crate::style::Dimension; +use crate::style::Display; +use crate::style::LengthPercentage; +use crate::style::LengthPercentageAuto; +use crate::style::Overflow; +use crate::style::Position; +use crate::Taffy; +use cssparser::Parser; +use cssparser::Token; + +impl CssValue for f32 { + fn parse<'i, 't>(input: &mut Parser<'i, 't>, _taffy: &Taffy) -> Result> { + Ok(input.expect_number()?) + } + + fn serialize(&self, dest: &mut String, _taffy: &Taffy) { + // `unwrap` should never panic since `impl fmt::Write for String` never returns `Err` + write!(dest, "{}", self).unwrap() + } +} + +pub(crate) enum MaybeAuto { + Auto, + NotAuto(T), +} + +impl CssValue for MaybeAuto { + fn parse<'i, 't>(input: &mut Parser<'i, 't>, taffy: &Taffy) -> Result> { + let state = input.state(); + if let Ok(()) = input.expect_ident_matching("auto") { + return Ok(Self::Auto); + } + input.reset(&state); + Ok(Self::NotAuto(T::parse(input, taffy)?)) + } + + fn serialize(&self, dest: &mut String, taffy: &Taffy) { + match self { + Self::Auto => dest.push_str("auto"), + Self::NotAuto(x) => x.serialize(dest, taffy), + } + } +} + +pub(crate) struct NonNegative(pub(crate) T); + +// Constrains type inference +pub(crate) fn non_negative(value: NonNegative) -> T { + value.0 +} + +trait IsNonNegative { + fn is_non_negative(&self) -> bool; +} + +impl IsNonNegative for f32 { + fn is_non_negative(&self) -> bool { + *self >= 0. + } +} + +impl IsNonNegative for LengthPercentage { + fn is_non_negative(&self) -> bool { + match *self { + Self::Length(x) => x >= 0., + Self::Percent(x) => x >= 0., + } + } +} + +impl IsNonNegative for LengthPercentageAuto { + fn is_non_negative(&self) -> bool { + match *self { + Self::Length(x) => x >= 0., + Self::Percent(x) => x >= 0., + Self::Auto => true, + } + } +} + +impl IsNonNegative for Dimension { + fn is_non_negative(&self) -> bool { + match *self { + Self::Length(x) => x >= 0., + Self::Percent(x) => x >= 0., + Self::Auto => true, + } + } +} + +impl IsNonNegative for Rect { + fn is_non_negative(&self) -> bool { + let Self { top, right, bottom, left } = self; + top.is_non_negative() && right.is_non_negative() && bottom.is_non_negative() && left.is_non_negative() + } +} + +impl CssValue for NonNegative { + fn parse<'i, 't>(input: &mut Parser<'i, 't>, taffy: &Taffy) -> Result> { + let value = T::parse(input, taffy)?; + if value.is_non_negative() { + Ok(NonNegative(value)) + } else { + Err(input.new_custom_error(ParseErrorKind::NegativeValue)) + } + } + + fn serialize(&self, dest: &mut String, taffy: &Taffy) { + self.0.serialize(dest, taffy) + } +} + +pub(crate) struct Ratio { + numerator: f32, + denominator: f32, +} + +impl CssValue for Ratio { + fn parse<'i, 't>(input: &mut Parser<'i, 't>, taffy: &Taffy) -> Result> { + let numerator = non_negative(CssValue::parse(input, taffy)?); + let denominator = if input.is_exhausted() { 1.0 } else { non_negative(CssValue::parse(input, taffy)?) }; + Ok(Self { numerator, denominator }) + } + + fn serialize(&self, dest: &mut String, taffy: &Taffy) { + self.numerator.serialize(dest, taffy); + if self.denominator != 1.0 { + dest.push_str(" / "); + self.denominator.serialize(dest, taffy) + } + } +} + +impl MaybeAuto { + pub(crate) fn to_opt_f32(self) -> Option { + match self { + MaybeAuto::Auto => None, + MaybeAuto::NotAuto(ratio) => Some(ratio.numerator / ratio.denominator), + } + } +} + +/// Assumes ` ?` syntax where a single value sets both components +impl CssValue for Point { + fn parse<'i, 't>(input: &mut Parser<'i, 't>, taffy: &Taffy) -> Result> { + let x = T::parse(input, taffy)?; + let y = if input.is_exhausted() { x.clone() } else { T::parse(input, taffy)? }; + Ok(Point { x, y }) + } + + fn serialize(&self, dest: &mut String, taffy: &Taffy) { + self.x.serialize(dest, taffy); + if self.x != self.y { + dest.push(' '); + self.y.serialize(dest, taffy); + } + } +} + +/// Assumes ` ( ( ?)?)?` syntax like `margin` etc. +/// +/// +impl CssValue for Rect { + fn parse<'i, 't>(input: &mut Parser<'i, 't>, taffy: &Taffy) -> Result> { + let top = T::parse(input, taffy)?; + let right = if input.is_exhausted() { top.clone() } else { T::parse(input, taffy)? }; + let bottom = if input.is_exhausted() { top.clone() } else { T::parse(input, taffy)? }; + let left = if input.is_exhausted() { right.clone() } else { T::parse(input, taffy)? }; + Ok(Rect { top, right, bottom, left }) + } + + fn serialize(&self, dest: &mut String, taffy: &Taffy) { + let right_is_needed = self.right != self.top; + let bottom_is_needed = self.bottom != self.top; + let left_is_needed = self.left != self.right; + self.top.serialize(dest, taffy); + if right_is_needed || bottom_is_needed || left_is_needed { + dest.push(' '); + self.right.serialize(dest, taffy); + if bottom_is_needed || left_is_needed { + dest.push(' '); + self.bottom.serialize(dest, taffy); + if left_is_needed { + dest.push(' '); + self.left.serialize(dest, taffy); + } + } + } + } +} + +impl CssValue for LengthPercentage { + fn parse<'i, 't>(input: &mut Parser<'i, 't>, taffy: &Taffy) -> Result> { + let token = input.next()?.clone(); + match &token { + Token::Dimension { value, unit, .. } => { + // We do not support relative length units are not supported + // Absolute units all have a fix ratio to each other: + // https://drafts.csswg.org/css-values/#absolute-lengths + let units_per_inch = match_ignore_ascii_case! { &*unit, + "px" => 96., + "pt" => 72., // point + "pc" => 6., // pica + "in" => 1., + "cm" => 2.54, + "mm" => 25.4, + "q" => 25.4 * 4., // quarter millimeter + _ => return Err(input.new_unexpected_token_error(token)) + }; + let css_inches = value / units_per_inch; + let css_pxs = css_inches * 96.; + let taffy_units = css_pxs * taffy.config.pixel_ratio; + Ok(Self::Length(taffy_units)) + } + Token::Percentage { unit_value, .. } => Ok(LengthPercentage::Percent(*unit_value)), + _ => Err(input.new_unexpected_token_error(token)), + } + } + + fn serialize(&self, dest: &mut String, taffy: &Taffy) { + match *self { + LengthPercentage::Length(taffy_units) => { + let css_pxs: f32 = taffy_units / taffy.config.pixel_ratio; + css_pxs.serialize(dest, taffy); + dest.push_str("px") + } + LengthPercentage::Percent(percent) => { + percent.serialize(dest, taffy); + dest.push('%') + } + } + } +} + +impl CssValue for LengthPercentageAuto { + fn parse<'i, 't>(input: &mut Parser<'i, 't>, taffy: &Taffy) -> Result> { + Ok(match CssValue::parse(input, taffy)? { + MaybeAuto::Auto => Self::Auto, + MaybeAuto::NotAuto(LengthPercentage::Length(x)) => Self::Length(x), + MaybeAuto::NotAuto(LengthPercentage::Percent(x)) => Self::Percent(x), + }) + } + + fn serialize(&self, dest: &mut String, taffy: &Taffy) { + match *self { + Self::Length(x) => MaybeAuto::NotAuto(LengthPercentage::Length(x)), + Self::Percent(x) => MaybeAuto::NotAuto(LengthPercentage::Percent(x)), + Self::Auto => MaybeAuto::Auto, + } + .serialize(dest, taffy) + } +} + +impl CssValue for Dimension { + fn parse<'i, 't>(input: &mut Parser<'i, 't>, taffy: &Taffy) -> Result> { + Ok(match CssValue::parse(input, taffy)? { + MaybeAuto::Auto => Self::Auto, + MaybeAuto::NotAuto(LengthPercentage::Length(x)) => Self::Length(x), + MaybeAuto::NotAuto(LengthPercentage::Percent(x)) => Self::Percent(x), + }) + } + + fn serialize(&self, dest: &mut String, taffy: &Taffy) { + match *self { + Self::Length(x) => MaybeAuto::NotAuto(LengthPercentage::Length(x)), + Self::Percent(x) => MaybeAuto::NotAuto(LengthPercentage::Percent(x)), + Self::Auto => MaybeAuto::Auto, + } + .serialize(dest, taffy) + } +} + +macro_rules! keywords_only { + ( $( + $( #[$meta: meta] )* + $keyword: literal => $enum_variant: ident, + )+ ) => { + fn parse<'i, 't>(input: &mut Parser<'i, 't>, _taffy: &Taffy) -> Result> { + let ident = input.expect_ident()?; + match_ignore_ascii_case! { &*ident, + $( + $( #[$meta] )* + $keyword => return Ok(Self::$enum_variant), + )+ + _ => { + let error = ParseErrorKind::InvalidOrUnknownKeyword(ident.clone()); + Err(input.new_custom_error(error)) + } + } + } + + fn serialize(&self, dest: &mut String, _taffy: &Taffy) { + dest.push_str(match self { + $( + $( #[$meta] )* + Self::$enum_variant => $keyword, + )+ + }) + } + }; +} + +impl CssValue for Display { + keywords_only! { + #[cfg(feature = "flexbox")] + "flex" => Flex, + #[cfg(feature = "grid")] + "grid" => Grid, + "none" => None, + } +} + +impl CssValue for Overflow { + keywords_only! { + "visible" => Visible, + "hidden" => Hidden, + } +} + +impl CssValue for Position { + keywords_only! { + "relative" => Relative, + "absolute" => Absolute, + } +} diff --git a/src/style/mod.rs b/src/style/mod.rs index ec0c5d039..c25e6e891 100644 --- a/src/style/mod.rs +++ b/src/style/mod.rs @@ -5,6 +5,9 @@ mod dimension; #[cfg(feature = "flexbox")] mod flex; +#[cfg(feature = "css-syntax")] +mod css_syntax; + pub use self::alignment::{AlignContent, AlignItems, AlignSelf, JustifyContent, JustifyItems, JustifySelf}; pub use self::dimension::{AvailableSpace, Dimension, LengthPercentage, LengthPercentageAuto}; diff --git a/src/tree/taffy_tree/tree.rs b/src/tree/taffy_tree/tree.rs index 7775a7d09..07b2e8999 100644 --- a/src/tree/taffy_tree/tree.rs +++ b/src/tree/taffy_tree/tree.rs @@ -16,11 +16,13 @@ use super::{TaffyError, TaffyResult}; pub(crate) struct TaffyConfig { /// Whether to round layout values pub(crate) use_rounding: bool, + /// Number of internal absolute units per CSS `px` + pub(crate) pixel_ratio: f32, } impl Default for TaffyConfig { fn default() -> Self { - Self { use_rounding: true } + Self { use_rounding: true, pixel_ratio: 1.0 } } } @@ -154,6 +156,26 @@ impl Taffy { self.config.use_rounding = false; } + /// Returns the current pixel ratio: the number of internal length units per CSS `px`. + /// + /// This affects the meaning of CSS absolute length units such as `px`, `pt`, `mm`, etc + /// when parsing or serializing [`Style`] in CSS syntax. + /// + /// The default is 1.0. + pub fn pixel_ratio(&self) -> f32 { + self.config.pixel_ratio + } + + /// Sets current pixel ratio: the number of internal length units per CSS `px`. + /// + /// This affects the meaning of CSS absolute length units such as `px`, `pt`, `mm`, etc + /// when parsing or serializing [`Style`] in CSS syntax. + /// + /// The default is 1.0. + pub fn set_pixel_ratio(&mut self, new_pixel_ratio: f32) { + self.config.pixel_ratio = new_pixel_ratio + } + /// Creates and adds a new unattached leaf node to the tree, and returns the node of the new node pub fn new_leaf(&mut self, layout: Style) -> TaffyResult { let id = self.nodes.insert(NodeData::new(layout));