diff options
-rw-r--r-- | components/style/build_gecko.rs | 11 | ||||
-rw-r--r-- | components/style/gecko/generated/bindings.rs | 12 | ||||
-rw-r--r-- | components/style/properties/gecko.mako.rs | 138 | ||||
-rw-r--r-- | components/style/properties/longhand/position.mako.rs | 11 | ||||
-rw-r--r-- | components/style/properties/shorthand/position.mako.rs | 4 | ||||
-rw-r--r-- | components/style/values/computed/mod.rs | 8 | ||||
-rw-r--r-- | components/style/values/specified/grid.rs | 663 | ||||
-rw-r--r-- | components/style/values/specified/mod.rs | 8 | ||||
-rw-r--r-- | components/style_traits/values.rs | 1 | ||||
-rw-r--r-- | tests/unit/style/parsing/mod.rs | 32 | ||||
-rw-r--r-- | tests/unit/style/parsing/position.rs | 67 |
11 files changed, 907 insertions, 48 deletions
diff --git a/components/style/build_gecko.rs b/components/style/build_gecko.rs index fa08db83622..0812e902707 100644 --- a/components/style/build_gecko.rs +++ b/components/style/build_gecko.rs @@ -662,7 +662,8 @@ mod bindings { .header(add_include("mozilla/ServoBindings.h")) .hide_type("nsACString_internal") .hide_type("nsAString_internal") - .raw_line("pub use nsstring::{nsACString, nsAString, nsString};") + .raw_line("pub use nsstring::{nsACString, nsAString, nsString, nsStringRepr};") + .raw_line("use gecko_bindings::structs::nsTArray;") .raw_line("type nsACString_internal = nsACString;") .raw_line("type nsAString_internal = nsAString;") .whitelisted_function("Servo_.*") @@ -859,7 +860,13 @@ mod bindings { // type with zero_size_type. If we ever introduce immutable borrow types // which _do_ need to be opaque, we'll need a separate mode. } - write_binding_file(builder, BINDINGS_FILE, &Vec::new()); + let fixups = vec![ + Fixup { // hack for gecko-owned string + pat: "<nsString".into(), + rep: "<nsStringRepr".into() + }, + ]; + write_binding_file(builder, BINDINGS_FILE, &fixups); } fn generate_atoms() { diff --git a/components/style/gecko/generated/bindings.rs b/components/style/gecko/generated/bindings.rs index 2008a696f11..ee99ffb4fe0 100644 --- a/components/style/gecko/generated/bindings.rs +++ b/components/style/gecko/generated/bindings.rs @@ -1,6 +1,7 @@ /* automatically generated by rust-bindgen */ -pub use nsstring::{nsACString, nsAString, nsString}; +pub use nsstring::{nsACString, nsAString, nsString, nsStringRepr}; +use gecko_bindings::structs::nsTArray; type nsACString_internal = nsACString; type nsAString_internal = nsAString; use gecko_bindings::structs::mozilla::css::GridTemplateAreasValue; @@ -944,6 +945,15 @@ extern "C" { pub fn Gecko_DropElementSnapshot(snapshot: ServoElementSnapshotOwned); } extern "C" { + pub fn Gecko_ResizeTArrayForStrings(array: *mut nsTArray<nsStringRepr>, + length: u32); +} +extern "C" { + pub fn Gecko_SetStyleGridTemplateArrayLengths(grid_template: + *mut nsStyleGridTemplate, + track_sizes: u32); +} +extern "C" { pub fn Gecko_CopyStyleGridTemplateValues(grid_template: *mut nsStyleGridTemplate, other: diff --git a/components/style/properties/gecko.mako.rs b/components/style/properties/gecko.mako.rs index fa58b6b9c98..599605c23af 100644 --- a/components/style/properties/gecko.mako.rs +++ b/components/style/properties/gecko.mako.rs @@ -1053,7 +1053,7 @@ fn static_assert() { skip_longhands="${skip_position_longhands} z-index box-sizing order align-content justify-content align-self justify-self align-items justify-items grid-auto-rows grid-auto-columns grid-auto-flow - grid-template-areas"> + grid-template-areas grid-template-rows grid-template-columns"> % for side in SIDES: <% impl_split_style_coord("%s" % side.ident, "mOffset", @@ -1165,9 +1165,9 @@ fn static_assert() { let ident = v.ident.unwrap_or(String::new()); self.gecko.${value.gecko}.mLineName.assign_utf8(&ident); self.gecko.${value.gecko}.mHasSpan = v.is_span; - self.gecko.${value.gecko}.mInteger = v.integer.map(|i| { + self.gecko.${value.gecko}.mInteger = v.line_num.map(|i| { // clamping the integer between a range - cmp::max(nsStyleGridLine_kMinLine, cmp::min(i, nsStyleGridLine_kMaxLine)) + cmp::max(nsStyleGridLine_kMinLine, cmp::min(i.value(), nsStyleGridLine_kMaxLine)) }).unwrap_or(0); } @@ -1179,7 +1179,7 @@ fn static_assert() { % endfor % for kind in ["rows", "columns"]: - pub fn set_grid_auto_${kind}(&mut self, v: longhands::grid_auto_rows::computed_value::T) { + pub fn set_grid_auto_${kind}(&mut self, v: longhands::grid_auto_${kind}::computed_value::T) { use values::specified::grid::TrackSize; match v { @@ -1206,6 +1206,136 @@ fn static_assert() { self.gecko.mGridAuto${kind.title()}Min.copy_from(&other.gecko.mGridAuto${kind.title()}Min); self.gecko.mGridAuto${kind.title()}Max.copy_from(&other.gecko.mGridAuto${kind.title()}Max); } + + pub fn set_grid_template_${kind}(&mut self, v: longhands::grid_template_${kind}::computed_value::T) { + <% self_grid = "self.gecko.mGridTemplate%s" % kind.title() %> + use gecko::values::GeckoStyleCoordConvertible; + use gecko_bindings::structs::{nsTArray, nsStyleGridLine_kMaxLine}; + use nsstring::{nsCString, nsStringRepr}; + use std::usize; + use values::specified::grid::TrackListType::Auto; + use values::specified::grid::{RepeatCount, TrackSize}; + + #[inline] + fn set_bitfield(bitfield: &mut u8, pos: u8, val: bool) { + let mask = 1 << (pos - 1); + *bitfield &= !mask; + *bitfield |= (val as u8) << (pos - 1); + } + + #[inline] + fn set_line_names(servo_names: &[String], gecko_names: &mut nsTArray<nsStringRepr>) { + unsafe { + bindings::Gecko_ResizeTArrayForStrings(gecko_names, servo_names.len() as u32); + } + + for (servo_name, gecko_name) in servo_names.iter().zip(gecko_names.iter_mut()) { + gecko_name.assign_utf8(&nsCString::from(&*servo_name)); + } + } + + fn set_track_size<G, T>(value: TrackSize<T>, gecko_min: &mut G, gecko_max: &mut G) + where G: CoordDataMut, T: GeckoStyleCoordConvertible + { + match value { + TrackSize::FitContent(lop) => { + gecko_min.set_value(CoordDataValue::None); + lop.to_gecko_style_coord(gecko_max); + }, + TrackSize::Breadth(breadth) => { + breadth.to_gecko_style_coord(gecko_min); + breadth.to_gecko_style_coord(gecko_max); + }, + TrackSize::MinMax(min, max) => { + min.to_gecko_style_coord(gecko_min); + max.to_gecko_style_coord(gecko_max); + }, + } + } + + // Set defaults + ${self_grid}.mRepeatAutoIndex = -1; + set_bitfield(&mut ${self_grid}._bitfield_1, 1, false); // mIsAutoFill + set_bitfield(&mut ${self_grid}._bitfield_1, 2, false); // mIsSubgrid + // FIXME: mIsSubgrid is false only for <none>, but we don't support subgrid name lists at the moment. + + match v { + Either::First(track) => { + let mut auto_idx = usize::MAX; + let mut auto_track_size = None; + if let Auto(idx) = track.list_type { + auto_idx = idx as usize; + let auto_repeat = track.auto_repeat.as_ref().expect("expected <auto-track-repeat> value"); + + if auto_repeat.count == RepeatCount::AutoFill { + set_bitfield(&mut ${self_grid}._bitfield_1, 1, true); + } + + ${self_grid}.mRepeatAutoIndex = idx as i16; + // NOTE: Gecko supports only one set of values in <auto-repeat> + // i.e., it can only take repeat(auto-fill, [a] 10px [b]), and no more. + set_line_names(&auto_repeat.line_names[0], &mut ${self_grid}.mRepeatAutoLineNameListBefore); + set_line_names(&auto_repeat.line_names[1], &mut ${self_grid}.mRepeatAutoLineNameListAfter); + auto_track_size = Some(auto_repeat.track_sizes.get(0).unwrap().clone()); + } else { + unsafe { + bindings::Gecko_ResizeTArrayForStrings( + &mut ${self_grid}.mRepeatAutoLineNameListBefore, 0); + bindings::Gecko_ResizeTArrayForStrings( + &mut ${self_grid}.mRepeatAutoLineNameListAfter, 0); + } + } + + let mut num_values = track.values.len(); + if auto_track_size.is_some() { + num_values += 1; + } + + let max_lines = nsStyleGridLine_kMaxLine as usize - 1; // for accounting the final <line-names> + num_values = cmp::min(num_values, max_lines); + unsafe { + bindings::Gecko_SetStyleGridTemplateArrayLengths(&mut ${self_grid}, num_values as u32); + } + + let mut line_names = track.line_names.into_iter(); + let mut values_iter = track.values.into_iter(); + let min_max_iter = ${self_grid}.mMinTrackSizingFunctions.iter_mut() + .zip(${self_grid}.mMaxTrackSizingFunctions.iter_mut()); + + for (i, (gecko_min, gecko_max)) in min_max_iter.enumerate().take(max_lines) { + let name_list = line_names.next().expect("expected line-names"); + set_line_names(&name_list, &mut ${self_grid}.mLineNameLists[i]); + if i == auto_idx { + set_track_size(auto_track_size.take().expect("expected <track-size> for <auto-track-repeat>"), + gecko_min, gecko_max); + continue + } + + let track_size = values_iter.next().expect("expected <track-size> value"); + set_track_size(track_size, gecko_min, gecko_max); + } + + let final_names = line_names.next().unwrap(); + set_line_names(&final_names, ${self_grid}.mLineNameLists.last_mut().unwrap()); + }, + Either::Second(_none) => { + unsafe { + bindings::Gecko_SetStyleGridTemplateArrayLengths(&mut ${self_grid}, 0); + bindings::Gecko_ResizeTArrayForStrings( + &mut ${self_grid}.mRepeatAutoLineNameListBefore, 0); + bindings::Gecko_ResizeTArrayForStrings( + &mut ${self_grid}.mRepeatAutoLineNameListAfter, 0); + } + }, + } + } + + pub fn copy_grid_template_${kind}_from(&mut self, other: &Self) { + unsafe { + bindings::Gecko_CopyStyleGridTemplateValues(&mut ${self_grid}, + &other.gecko.mGridTemplate${kind.title()}); + } + } % endfor pub fn set_grid_auto_flow(&mut self, v: longhands::grid_auto_flow::computed_value::T) { diff --git a/components/style/properties/longhand/position.mako.rs b/components/style/properties/longhand/position.mako.rs index 370a538e893..0cea82301cc 100644 --- a/components/style/properties/longhand/position.mako.rs +++ b/components/style/properties/longhand/position.mako.rs @@ -319,6 +319,17 @@ ${helpers.predefined_type("object-position", spec="https://drafts.csswg.org/css-grid/#propdef-grid-auto-%ss" % kind, products="gecko", boxed=True)} + + // NOTE: The spec lists only `none | <track-list> | <auto-track-list>`, but gecko seems to support + // `subgrid <line-name-list>?` in addition to this (probably old spec). We should support it soon. + ${helpers.predefined_type("grid-template-%ss" % kind, + "TrackListOrNone", + "Either::Second(None_)", + products="gecko", + spec="https://drafts.csswg.org/css-grid/#propdef-grid-template-%ss" % kind, + boxed=True, + animation_value_type="none")} + % endfor <%helpers:longhand name="grid-auto-flow" diff --git a/components/style/properties/shorthand/position.mako.rs b/components/style/properties/shorthand/position.mako.rs index 017b592edea..58206bb67f4 100644 --- a/components/style/properties/shorthand/position.mako.rs +++ b/components/style/properties/shorthand/position.mako.rs @@ -154,7 +154,7 @@ GridLine::parse(context, input)? } else { let mut line = GridLine::default(); - if start.integer.is_none() && !start.is_span { + if start.line_num.is_none() && !start.is_span { line.ident = start.ident.clone(); // ident from start value should be taken } @@ -188,7 +188,7 @@ pub fn parse_value(context: &ParserContext, input: &mut Parser) -> Result<Longhands, ()> { fn line_with_ident_from(other: &GridLine) -> GridLine { let mut this = GridLine::default(); - if other.integer.is_none() && !other.is_span { + if other.line_num.is_none() && !other.is_span { this.ident = other.ident.clone(); } diff --git a/components/style/values/computed/mod.rs b/components/style/values/computed/mod.rs index 02a71c6b313..7101192be1a 100644 --- a/components/style/values/computed/mod.rs +++ b/components/style/values/computed/mod.rs @@ -19,6 +19,7 @@ use super::{CSSFloat, CSSInteger, RGBA}; use super::generics::BorderRadiusSize as GenericBorderRadiusSize; use super::specified; use super::specified::grid::{TrackBreadth as GenericTrackBreadth, TrackSize as GenericTrackSize}; +use super::specified::grid::TrackList as GenericTrackList; pub use app_units::Au; pub use cssparser::Color as CSSColor; @@ -595,6 +596,13 @@ pub type TrackBreadth = GenericTrackBreadth<LengthOrPercentage>; /// The computed value of a grid `<track-size>` pub type TrackSize = GenericTrackSize<LengthOrPercentage>; +/// The computed value of a grid `<track-list>` +/// (could also be `<auto-track-list>` or `<explicit-track-list>`) +pub type TrackList = GenericTrackList<TrackSize>; + +/// `<track-list> | none` +pub type TrackListOrNone = Either<TrackList, None_>; + impl ClipRectOrAuto { /// Return an auto (default for clip-rect and image-region) value pub fn auto() -> Self { diff --git a/components/style/values/specified/grid.rs b/components/style/values/specified/grid.rs index 92dbc4fbcaa..45f888d9702 100644 --- a/components/style/values/specified/grid.rs +++ b/components/style/values/specified/grid.rs @@ -4,21 +4,20 @@ //! Necessary types for [grid](https://drafts.csswg.org/css-grid/). -use cssparser::{Parser, Token}; +use cssparser::{Parser, Token, serialize_identifier}; use parser::{Parse, ParserContext}; +use std::{fmt, mem, usize}; use std::ascii::AsciiExt; -use std::fmt; use style_traits::ToCss; -use values::{CSSFloat, HasViewportPercentage}; -use values::computed::{ComputedValueAsSpecified, Context, ToComputedValue}; -use values::specified::LengthOrPercentage; +use values::{CSSFloat, CustomIdent, Either, HasViewportPercentage}; +use values::computed::{self, ComputedValueAsSpecified, Context, ToComputedValue}; +use values::specified::{Integer, LengthOrPercentage}; #[derive(PartialEq, Clone, Debug)] #[cfg_attr(feature = "servo", derive(HeapSizeOf))] /// A `<grid-line>` type. /// /// https://drafts.csswg.org/css-grid/#typedef-grid-row-start-grid-line -#[allow(missing_docs)] pub struct GridLine { /// Flag to check whether it's a `span` keyword. pub is_span: bool, @@ -27,7 +26,14 @@ pub struct GridLine { /// https://drafts.csswg.org/css-grid/#grid-placement-slot pub ident: Option<String>, /// Denotes the nth grid line from grid item's placement. - pub integer: Option<i32>, + pub line_num: Option<Integer>, +} + +impl GridLine { + /// Check whether this `<grid-line>` represents an `auto` value. + pub fn is_auto(&self) -> bool { + self.ident.is_none() && self.line_num.is_none() && !self.is_span + } } impl Default for GridLine { @@ -35,27 +41,28 @@ impl Default for GridLine { GridLine { is_span: false, ident: None, - integer: None, + line_num: None, } } } impl ToCss for GridLine { fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { - if !self.is_span && self.ident.is_none() && self.integer.is_none() { + if self.is_auto() { return dest.write_str("auto") } if self.is_span { - try!(dest.write_str("span")); + dest.write_str("span")?; } - if let Some(i) = self.integer { - try!(write!(dest, " {}", i)); + if let Some(i) = self.line_num { + write!(dest, " {}", i.value)?; } if let Some(ref s) = self.ident { - try!(write!(dest, " {}", s)); + dest.write_str(" ")?; + serialize_identifier(s, dest)?; } Ok(()) @@ -63,7 +70,7 @@ impl ToCss for GridLine { } impl Parse for GridLine { - fn parse(_context: &ParserContext, input: &mut Parser) -> Result<Self, ()> { + fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ()> { let mut grid_line = Default::default(); if input.try(|i| i.expect_ident_matching("auto")).is_ok() { return Ok(grid_line) @@ -71,17 +78,17 @@ impl Parse for GridLine { for _ in 0..3 { // Maximum possible entities for <grid-line> if input.try(|i| i.expect_ident_matching("span")).is_ok() { - if grid_line.is_span { - return Err(()) + if grid_line.is_span || grid_line.line_num.is_some() || grid_line.ident.is_some() { + return Err(()) // span (if specified) should be first } - grid_line.is_span = true; - } else if let Ok(i) = input.try(|i| i.expect_integer()) { - if i == 0 || grid_line.integer.is_some() { + grid_line.is_span = true; // span (if specified) should be first + } else if let Ok(i) = input.try(|i| Integer::parse(context, i)) { + if i.value == 0 || grid_line.line_num.is_some() { return Err(()) } - grid_line.integer = Some(i); + grid_line.line_num = Some(i); } else if let Ok(name) = input.try(|i| i.expect_ident()) { - if grid_line.ident.is_some() { + if grid_line.ident.is_some() || CustomIdent::from_ident((&*name).into(), &[]).is_err() { return Err(()) } grid_line.ident = Some(name.into_owned()); @@ -90,13 +97,19 @@ impl Parse for GridLine { } } + if grid_line.is_auto() { + return Err(()) + } + if grid_line.is_span { - if let Some(i) = grid_line.integer { - if i < 0 { // disallow negative integers for grid spans + if let Some(i) = grid_line.line_num { + if i.value <= 0 { // disallow negative integers for grid spans return Err(()) } + } else if grid_line.ident.is_some() { // integer could be omitted + grid_line.line_num = Some(Integer::new(1)); } else { - grid_line.integer = Some(1); + return Err(()) } } @@ -128,9 +141,22 @@ pub enum TrackBreadth<L> { Keyword(TrackKeyword), } +impl<L> TrackBreadth<L> { + /// Check whether this is a `<fixed-breadth>` (i.e., it only has `<length-percentage>`) + /// + /// https://drafts.csswg.org/css-grid/#typedef-fixed-breadth + #[inline] + pub fn is_fixed(&self) -> bool { + match *self { + TrackBreadth::Breadth(ref _lop) => true, + _ => false, + } + } +} + /// Parse a single flexible length. pub fn parse_flex(input: &mut Parser) -> Result<CSSFloat, ()> { - match try!(input.next()) { + match input.next()? { Token::Dimension(ref value, ref unit) if unit.eq_ignore_ascii_case("fr") && value.value.is_sign_positive() => Ok(value.value), _ => Err(()), @@ -215,6 +241,32 @@ pub enum TrackSize<L> { FitContent(L), } +impl<L> TrackSize<L> { + /// Check whether this is a `<fixed-size>` + /// + /// https://drafts.csswg.org/css-grid/#typedef-fixed-size + pub fn is_fixed(&self) -> bool { + match *self { + TrackSize::Breadth(ref breadth) => breadth.is_fixed(), + // For minmax function, it could be either + // minmax(<fixed-breadth>, <track-breadth>) or minmax(<inflexible-breadth>, <fixed-breadth>), + // and since both variants are a subset of minmax(<inflexible-breadth>, <track-breadth>), we only + // need to make sure that they're fixed. So, we don't have to modify the parsing function. + TrackSize::MinMax(ref breadth_1, ref breadth_2) => { + if breadth_1.is_fixed() { + return true // the second value is always a <track-breadth> + } + + match *breadth_1 { + TrackBreadth::Flex(_) => false, // should be <inflexible-breadth> at this point + _ => breadth_2.is_fixed(), + } + }, + TrackSize::FitContent(_) => false, + } + } +} + impl<L> Default for TrackSize<L> { fn default() -> Self { TrackSize::Breadth(TrackBreadth::Keyword(TrackKeyword::Auto)) @@ -233,19 +285,19 @@ impl Parse for TrackSize<LengthOrPercentage> { match input.try(|i| LengthOrPercentage::parse_non_negative(context, i)) { Ok(lop) => TrackBreadth::Breadth(lop), Err(..) => { - let keyword = try!(TrackKeyword::parse(input)); + let keyword = TrackKeyword::parse(input)?; TrackBreadth::Keyword(keyword) } }; - try!(input.expect_comma()); - Ok(TrackSize::MinMax(inflexible_breadth, try!(TrackBreadth::parse(context, input)))) + input.expect_comma()?; + Ok(TrackSize::MinMax(inflexible_breadth, TrackBreadth::parse(context, input)?)) }); } - try!(input.expect_function_matching("fit-content")); - // FIXME(emilio): This needs a parse_nested_block, doesn't it? - Ok(try!(LengthOrPercentage::parse(context, input).map(TrackSize::FitContent))) + input.expect_function_matching("fit-content")?; + let lop = input.parse_nested_block(|i| LengthOrPercentage::parse_non_negative(context, i))?; + Ok(TrackSize::FitContent(lop)) } } @@ -254,15 +306,15 @@ impl<L: ToCss> ToCss for TrackSize<L> { match *self { TrackSize::Breadth(ref b) => b.to_css(dest), TrackSize::MinMax(ref infexible, ref flexible) => { - try!(dest.write_str("minmax(")); - try!(infexible.to_css(dest)); - try!(dest.write_str(",")); - try!(flexible.to_css(dest)); + dest.write_str("minmax(")?; + infexible.to_css(dest)?; + dest.write_str(", ")?; + flexible.to_css(dest)?; dest.write_str(")") }, TrackSize::FitContent(ref lop) => { - try!(dest.write_str("fit-content(")); - try!(lop.to_css(dest)); + dest.write_str("fit-content(")?; + lop.to_css(dest)?; dest.write_str(")") }, } @@ -286,7 +338,13 @@ impl<L: ToComputedValue> ToComputedValue for TrackSize<L> { #[inline] fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { match *self { - TrackSize::Breadth(ref b) => TrackSize::Breadth(b.to_computed_value(context)), + TrackSize::Breadth(ref b) => match *b { + // <flex> outside `minmax()` expands to `mimmax(auto, <flex>)` + // https://drafts.csswg.org/css-grid/#valdef-grid-template-columns-flex + TrackBreadth::Flex(f) => + TrackSize::MinMax(TrackBreadth::Keyword(TrackKeyword::Auto), TrackBreadth::Flex(f)), + _ => TrackSize::Breadth(b.to_computed_value(context)), + }, TrackSize::MinMax(ref b_1, ref b_2) => TrackSize::MinMax(b_1.to_computed_value(context), b_2.to_computed_value(context)), TrackSize::FitContent(ref lop) => TrackSize::FitContent(lop.to_computed_value(context)), @@ -306,3 +364,532 @@ impl<L: ToComputedValue> ToComputedValue for TrackSize<L> { } } } + +/// Parse the grid line names into a vector of owned strings. +/// +/// https://drafts.csswg.org/css-grid/#typedef-line-names +pub fn parse_line_names(input: &mut Parser) -> Result<Vec<String>, ()> { + input.expect_square_bracket_block()?; + input.parse_nested_block(|input| { + let mut values = vec![]; + while let Ok(ident) = input.try(|i| i.expect_ident()) { + if CustomIdent::from_ident((&*ident).into(), &["span"]).is_err() { + return Err(()) + } + + values.push(ident.into_owned()); + } + + Ok(values) + }) +} + +fn concat_serialize_idents<W>(prefix: &str, suffix: &str, + slice: &[String], sep: &str, dest: &mut W) -> fmt::Result + where W: fmt::Write +{ + if let Some((ref first, rest)) = slice.split_first() { + dest.write_str(prefix)?; + serialize_identifier(first, dest)?; + for thing in rest { + dest.write_str(sep)?; + serialize_identifier(thing, dest)?; + } + + dest.write_str(suffix)?; + } + + Ok(()) +} + +/// The initial argument of the `repeat` function. +/// +/// https://drafts.csswg.org/css-grid/#typedef-track-repeat +#[derive(Clone, Copy, PartialEq, Debug)] +#[cfg_attr(feature = "servo", derive(HeapSizeOf))] +pub enum RepeatCount { + /// A positive integer. This is allowed only for `<track-repeat>` and `<fixed-repeat>` + Number(Integer), + /// An `<auto-fill>` keyword allowed only for `<auto-repeat>` + AutoFill, + /// An `<auto-fit>` keyword allowed only for `<auto-repeat>` + AutoFit, +} + +impl Parse for RepeatCount { + fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ()> { + if let Ok(i) = input.try(|i| Integer::parse(context, i)) { + if i.value > 0 { + Ok(RepeatCount::Number(i)) + } else { + Err(()) + } + } else { + match_ignore_ascii_case! { &input.expect_ident()?, + "auto-fill" => Ok(RepeatCount::AutoFill), + "auto-fit" => Ok(RepeatCount::AutoFit), + _ => Err(()), + } + } + } +} + +impl ToCss for RepeatCount { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { + match *self { + RepeatCount::Number(ref c) => c.to_css(dest), + RepeatCount::AutoFill => dest.write_str("auto-fill"), + RepeatCount::AutoFit => dest.write_str("auto-fit"), + } + } +} + +impl ComputedValueAsSpecified for RepeatCount {} +no_viewport_percentage!(RepeatCount); + +/// The type of `repeat` function (only used in parsing). +/// +/// https://drafts.csswg.org/css-grid/#typedef-track-repeat +#[derive(Clone, Copy, PartialEq, Debug)] +#[cfg_attr(feature = "servo", derive(HeapSizeOf))] +enum RepeatType { + /// [`<auto-repeat>`](https://drafts.csswg.org/css-grid/#typedef-auto-repeat) + Auto, + /// [`<track-repeat>`](https://drafts.csswg.org/css-grid/#typedef-track-repeat) + Normal, + /// [`<fixed-repeat>`](https://drafts.csswg.org/css-grid/#typedef-fixed-repeat) + Fixed, +} + +/// The structure containing `<line-names>` and `<track-size>` values. +/// +/// It can also hold `repeat()` function parameters, which expands into the respective +/// values in its computed form. +#[derive(Clone, PartialEq, Debug)] +#[cfg_attr(feature = "servo", derive(HeapSizeOf))] +pub struct TrackRepeat<L> { + /// The number of times for the value to be repeated (could also be `auto-fit` or `auto-fill`) + pub count: RepeatCount, + /// `<line-names>` accompanying `<track_size>` values. + /// + /// If there's no `<line-names>`, then it's represented by an empty vector. + /// For N `<track-size>` values, there will be N+1 `<line-names>`, and so this vector's + /// length is always one value more than that of the `<track-size>`. + pub line_names: Vec<Vec<String>>, + /// `<track-size>` values. + pub track_sizes: Vec<TrackSize<L>>, +} + +impl TrackRepeat<LengthOrPercentage> { + fn parse_with_repeat_type(context: &ParserContext, input: &mut Parser) + -> Result<(TrackRepeat<LengthOrPercentage>, RepeatType), ()> { + input.try(|i| i.expect_function_matching("repeat")).and_then(|_| { + input.parse_nested_block(|input| { + let count = RepeatCount::parse(context, input)?; + input.expect_comma()?; + + let is_auto = count == RepeatCount::AutoFit || count == RepeatCount::AutoFill; + let mut repeat_type = if is_auto { + RepeatType::Auto + } else { // <fixed-size> is a subset of <track_size>, so it should work for both + RepeatType::Fixed + }; + + let mut names = vec![]; + let mut values = vec![]; + let mut current_names; + + loop { + current_names = input.try(parse_line_names).unwrap_or(vec![]); + if let Ok(track_size) = input.try(|i| TrackSize::parse(context, i)) { + if !track_size.is_fixed() { + if is_auto { + return Err(()) // should be <fixed-size> for <auto-repeat> + } + + if repeat_type == RepeatType::Fixed { + repeat_type = RepeatType::Normal // <track-size> for sure + } + } + + values.push(track_size); + names.push(current_names); + } else { + if values.is_empty() { + return Err(()) // expecting at least one <track-size> + } + + names.push(current_names); // final `<line-names>` + break // no more <track-size>, breaking + } + } + + let repeat = TrackRepeat { + count: count, + track_sizes: values, + line_names: names, + }; + + Ok((repeat, repeat_type)) + }) + }) + } +} + +impl<L: ToCss> ToCss for TrackRepeat<L> { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { + dest.write_str("repeat(")?; + self.count.to_css(dest)?; + dest.write_str(", ")?; + + let mut line_names_iter = self.line_names.iter(); + for (i, (ref size, ref names)) in self.track_sizes.iter() + .zip(&mut line_names_iter).enumerate() { + if i > 0 { + dest.write_str(" ")?; + } + + concat_serialize_idents("[", "] ", names, " ", dest)?; + size.to_css(dest)?; + } + + if let Some(line_names_last) = line_names_iter.next() { + concat_serialize_idents(" [", "]", line_names_last, " ", dest)?; + } + + dest.write_str(")")?; + Ok(()) + } +} + +impl HasViewportPercentage for TrackRepeat<LengthOrPercentage> { + #[inline] + fn has_viewport_percentage(&self) -> bool { + self.track_sizes.iter().any(|ref v| v.has_viewport_percentage()) + } +} + +impl<L: ToComputedValue> ToComputedValue for TrackRepeat<L> { + type ComputedValue = TrackRepeat<L::ComputedValue>; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + // If the repeat count is numeric, then expand the values and merge accordingly. + if let RepeatCount::Number(num) = self.count { + let mut line_names = vec![]; + let mut track_sizes = vec![]; + let mut prev_names = vec![]; + + for _ in 0..num.value { + let mut names_iter = self.line_names.iter(); + for (size, names) in self.track_sizes.iter().zip(&mut names_iter) { + prev_names.extend_from_slice(&names); + line_names.push(mem::replace(&mut prev_names, vec![])); + track_sizes.push(size.to_computed_value(context)); + } + + if let Some(names) = names_iter.next() { + prev_names.extend_from_slice(&names); + } + } + + line_names.push(prev_names); + TrackRepeat { + count: self.count, + track_sizes: track_sizes, + line_names: line_names, + } + + } else { // if it's auto-fit/auto-fill, then it's left to the layout. + TrackRepeat { + count: self.count, + track_sizes: self.track_sizes.iter() + .map(|l| l.to_computed_value(context)) + .collect(), + line_names: self.line_names.clone(), + } + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + TrackRepeat { + count: computed.count, + track_sizes: computed.track_sizes.iter() + .map(ToComputedValue::from_computed_value) + .collect(), + line_names: computed.line_names.clone(), + } + } +} + +/// The type of a `<track-list>` as determined during parsing. +/// +/// https://drafts.csswg.org/css-grid/#typedef-track-list +#[derive(Clone, Copy, PartialEq, Debug)] +#[cfg_attr(feature = "servo", derive(HeapSizeOf))] +pub enum TrackListType { + /// [`<auto-track-list>`](https://drafts.csswg.org/css-grid/#typedef-auto-track-list) + /// + /// If this type exists, then the value at the index in `line_names` field in `TrackList` + /// has the `<line-names>?` list that comes before `<auto-repeat>`. If it's a specified value, + /// then the `repeat()` function (that follows the line names list) is also at the given index + /// in `values` field. On the contrary, if it's a computed value, then the `repeat()` function + /// is in the `auto_repeat` field. + Auto(u16), + /// [`<track-list>`](https://drafts.csswg.org/css-grid/#typedef-track-list) + Normal, + /// [`<explicit-track-list>`](https://drafts.csswg.org/css-grid/#typedef-explicit-track-list) + /// + /// Note that this is a subset of the normal `<track-list>`, and so it could be used in place + /// of the latter. + Explicit, +} + +/// A grid `<track-list>` type. +/// +/// https://drafts.csswg.org/css-grid/#typedef-track-list +#[derive(Clone, PartialEq, Debug)] +#[cfg_attr(feature = "servo", derive(HeapSizeOf))] +pub struct TrackList<T> { + /// The type of this `<track-list>` (auto, explicit or general). + /// + /// In order to avoid parsing the same value multiple times, this does a single traversal + /// and arrives at the type of value it has parsed (or bails out gracefully with an error). + pub list_type: TrackListType, + /// A vector of `<track-size> | <track-repeat>` values. In its specified form, it may contain + /// any value, but once it's computed, it contains only `<track_size>` values. + /// + /// Note that this may also contain `<auto-repeat>` at an index. If it exists, it's + /// given by the index in `TrackListType::Auto` + pub values: Vec<T>, + /// `<line-names>` accompanying `<track-size> | <track-repeat>` values. + /// + /// If there's no `<line-names>`, then it's represented by an empty vector. + /// For N values, there will be N+1 `<line-names>`, and so this vector's + /// length is always one value more than that of the `<track-size>`. + pub line_names: Vec<Vec<String>>, + /// `<auto-repeat>` value after computation. This field is necessary, because + /// the `values` field (after computation) will only contain `<track-size>` values, and + /// we need something to represent this function. + pub auto_repeat: Option<TrackRepeat<computed::LengthOrPercentage>>, +} + +/// Either a `<track-size>` or `<track-repeat>` component of `<track-list>` +/// +/// This is required only for the specified form of `<track-list>`, and will become +/// `TrackSize<LengthOrPercentage>` in its computed form. +pub type TrackSizeOrRepeat = Either<TrackSize<LengthOrPercentage>, TrackRepeat<LengthOrPercentage>>; + +impl Parse for TrackList<TrackSizeOrRepeat> { + fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ()> { + let mut current_names; + let mut names = vec![]; + let mut values = vec![]; + + let mut list_type = TrackListType::Explicit; // assume it's the simplest case + // marker to check whether we've already encountered <auto-repeat> along the way + let mut is_auto = false; + // assume that everything is <fixed-size>. This flag is useful when we encounter <auto-repeat> + let mut atleast_one_not_fixed = false; + + loop { + current_names = input.try(parse_line_names).unwrap_or(vec![]); + if let Ok(track_size) = input.try(|i| TrackSize::parse(context, i)) { + if !track_size.is_fixed() { + atleast_one_not_fixed = true; + if is_auto { + return Err(()) // <auto-track-list> only accepts <fixed-size> and <fixed-repeat> + } + } + + names.push(current_names); + values.push(Either::First(track_size)); + } else if let Ok((repeat, type_)) = input.try(|i| TrackRepeat::parse_with_repeat_type(context, i)) { + if list_type == TrackListType::Explicit { + list_type = TrackListType::Normal; // <explicit-track-list> doesn't contain repeat() + } + + match type_ { + RepeatType::Normal => { + atleast_one_not_fixed = true; + if is_auto { // only <fixed-repeat> + return Err(()) + } + }, + RepeatType::Auto => { + if is_auto || atleast_one_not_fixed { + // We've either seen <auto-repeat> earlier, or there's at least one non-fixed value + return Err(()) + } + + is_auto = true; + list_type = TrackListType::Auto(values.len() as u16); + }, + RepeatType::Fixed => (), + } + + names.push(current_names); + values.push(Either::Second(repeat)); + } else { + if values.is_empty() { + return Err(()) + } + + names.push(current_names); + break + } + } + + Ok(TrackList { + list_type: list_type, + values: values, + line_names: names, + auto_repeat: None, // filled only in computation + }) + } +} + +impl<T: ToCss> ToCss for TrackList<T> { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { + let auto_idx = match self.list_type { + TrackListType::Auto(i) => i as usize, + _ => usize::MAX, + }; + + let mut values_iter = self.values.iter().peekable(); + let mut line_names_iter = self.line_names.iter().peekable(); + + for idx in 0.. { + let names = line_names_iter.next().unwrap(); // This should exist! + concat_serialize_idents("[", "]", names, " ", dest)?; + + match self.auto_repeat { + Some(ref repeat) if idx == auto_idx => { + if !names.is_empty() { + dest.write_str(" ")?; + } + + repeat.to_css(dest)?; + }, + _ => match values_iter.next() { + Some(value) => { + if !names.is_empty() { + dest.write_str(" ")?; + } + + value.to_css(dest)?; + }, + None => break, + }, + } + + if values_iter.peek().is_some() || line_names_iter.peek().map_or(false, |v| !v.is_empty()) { + dest.write_str(" ")?; + } + } + + Ok(()) + } +} + +impl HasViewportPercentage for TrackList<TrackSizeOrRepeat> { + #[inline] + fn has_viewport_percentage(&self) -> bool { + self.values.iter().any(|ref v| v.has_viewport_percentage()) + } +} + +impl ToComputedValue for TrackList<TrackSizeOrRepeat> { + type ComputedValue = TrackList<TrackSize<computed::LengthOrPercentage>>; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + // Merge the line names while computing values. The resulting values will + // all be a bunch of `<track-size>`. + // + // For example, + // `[a b] 100px [c d] repeat(1, 30px [g]) [h]` will be merged as `[a b] 100px [c d] 30px [g h]` + // whereas, `[a b] repeat(2, [c] 50px [d]) [e f] repeat(auto-fill, [g] 12px) 10px [h]` will be merged as + // `[a b c] 50px [d c] 50px [d e f] repeat(auto-fill, [g] 12px) 10px [h]`, with the `<auto-repeat>` value + // set in the `auto_repeat` field, and the `idx` in TrackListType::Auto pointing to the values after + // `<auto-repeat>` (in this case, `10px [h]`). + let mut line_names = vec![]; + let mut list_type = self.list_type; + let mut values = vec![]; + let mut prev_names = vec![]; + let mut auto_repeat = None; + + let mut names_iter = self.line_names.iter(); + for (size_or_repeat, names) in self.values.iter().zip(&mut names_iter) { + prev_names.extend_from_slice(names); + + match *size_or_repeat { + Either::First(ref size) => values.push(size.to_computed_value(context)), + Either::Second(ref repeat) => { + let mut computed = repeat.to_computed_value(context); + if computed.count == RepeatCount::AutoFit || computed.count == RepeatCount::AutoFill { + line_names.push(mem::replace(&mut prev_names, vec![])); // don't merge for auto + list_type = TrackListType::Auto(values.len() as u16); + auto_repeat = Some(computed); + continue + } + + let mut repeat_names_iter = computed.line_names.drain(..); + for (size, mut names) in computed.track_sizes.drain(..).zip(&mut repeat_names_iter) { + prev_names.append(&mut names); + line_names.push(mem::replace(&mut prev_names, vec![])); + values.push(size); + } + + if let Some(mut names) = repeat_names_iter.next() { + prev_names.append(&mut names); + } + + continue // last `<line-names>` in repeat() may merge with the next set + } + } + + line_names.push(mem::replace(&mut prev_names, vec![])); + } + + if let Some(names) = names_iter.next() { + prev_names.extend_from_slice(names); + } + + line_names.push(mem::replace(&mut prev_names, vec![])); + + TrackList { + list_type: list_type, + values: values, + line_names: line_names, + auto_repeat: auto_repeat, + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + let auto_idx = if let TrackListType::Auto(idx) = computed.list_type { + idx as usize + } else { + usize::MAX + }; + + let mut values = Vec::with_capacity(computed.values.len() + 1); + for (i, value) in computed.values.iter().map(ToComputedValue::from_computed_value).enumerate() { + if i == auto_idx { + let value = TrackRepeat::from_computed_value(computed.auto_repeat.as_ref().unwrap()); + values.push(Either::Second(value)); + } + + values.push(Either::First(value)); + } + + TrackList { + list_type: computed.list_type, + values: values, + line_names: computed.line_names.clone(), + auto_repeat: None, + } + } +} diff --git a/components/style/values/specified/mod.rs b/components/style/values/specified/mod.rs index 8726cb15334..3fb46c473b9 100644 --- a/components/style/values/specified/mod.rs +++ b/components/style/values/specified/mod.rs @@ -13,6 +13,7 @@ use euclid::size::Size2D; use itoa; use parser::{ParserContext, Parse}; use self::grid::{TrackBreadth as GenericTrackBreadth, TrackSize as GenericTrackSize}; +use self::grid::{TrackList as GenericTrackList, TrackSizeOrRepeat}; use self::url::SpecifiedUrl; use std::ascii::AsciiExt; use std::f32; @@ -942,6 +943,13 @@ pub type TrackBreadth = GenericTrackBreadth<LengthOrPercentage>; /// The specified value of a grid `<track-size>` pub type TrackSize = GenericTrackSize<LengthOrPercentage>; +/// The specified value of a grid `<track-list>` +/// (could also be `<auto-track-list>` or `<explicit-track-list>`) +pub type TrackList = GenericTrackList<TrackSizeOrRepeat>; + +/// `<track-list> | none` +pub type TrackListOrNone = Either<TrackList, None_>; + #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "servo", derive(HeapSizeOf))] #[allow(missing_docs)] diff --git a/components/style_traits/values.rs b/components/style_traits/values.rs index 847177c2aaa..aaff1a998ea 100644 --- a/components/style_traits/values.rs +++ b/components/style_traits/values.rs @@ -74,6 +74,7 @@ macro_rules! impl_to_css_for_predefined_type { impl_to_css_for_predefined_type!(f32); impl_to_css_for_predefined_type!(i32); +impl_to_css_for_predefined_type!(u16); impl_to_css_for_predefined_type!(u32); impl_to_css_for_predefined_type!(::cssparser::Token<'a>); impl_to_css_for_predefined_type!(::cssparser::RGBA); diff --git a/tests/unit/style/parsing/mod.rs b/tests/unit/style/parsing/mod.rs index e15d2dca21b..5f48483affa 100644 --- a/tests/unit/style/parsing/mod.rs +++ b/tests/unit/style/parsing/mod.rs @@ -5,10 +5,16 @@ //! Tests for parsing and serialization of values/properties use cssparser::Parser; +use euclid::size::TypedSize2D; use media_queries::CSSErrorReporterTest; use style::context::QuirksMode; +use style::font_metrics::ServoMetricsProvider; +use style::media_queries::{Device, MediaType}; use style::parser::{PARSING_MODE_DEFAULT, ParserContext}; +use style::properties::{ComputedValues, StyleBuilder}; use style::stylesheets::{CssRuleType, Origin}; +use style::values::computed::{Context, ToComputedValue}; +use style_traits::ToCss; fn parse<T, F: Fn(&ParserContext, &mut Parser) -> Result<T, ()>>(f: F, s: &str) -> Result<T, ()> { let url = ::servo_url::ServoUrl::parse("http://localhost").unwrap(); @@ -24,6 +30,32 @@ fn parse_entirely<T, F: Fn(&ParserContext, &mut Parser) -> Result<T, ()>>(f: F, parse(|context, parser| parser.parse_entirely(|p| f(context, p)), s) } +fn assert_computed_serialization<C, F, T>(f: F, input: &str, output: &str) + where F: Fn(&ParserContext, &mut Parser) -> Result<T, ()>, + T: ToComputedValue<ComputedValue=C>, C: ToCss +{ + let viewport_size = TypedSize2D::new(0., 0.); + let initial_style = ComputedValues::initial_values(); + let device = Device::new(MediaType::Screen, viewport_size); + + let context = Context { + is_root_element: true, + device: &device, + inherited_style: initial_style, + layout_parent_style: initial_style, + style: StyleBuilder::for_derived_style(&initial_style), + cached_system_font: None, + font_metrics_provider: &ServoMetricsProvider, + in_media_query: false, + quirks_mode: QuirksMode::NoQuirks, + }; + + let parsed = parse(f, input).unwrap(); + let computed = parsed.to_computed_value(&context); + let serialized = ToCss::to_css_string(&computed); + assert_eq!(serialized, output); +} + // This is a macro so that the file/line information // is preserved in the panic macro_rules! assert_roundtrip_with_context { diff --git a/tests/unit/style/parsing/position.rs b/tests/unit/style/parsing/position.rs index 7a3bfe1bae3..c1527c2f233 100644 --- a/tests/unit/style/parsing/position.rs +++ b/tests/unit/style/parsing/position.rs @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -use parsing::{parse, parse_entirely}; +use parsing::{assert_computed_serialization, parse, parse_entirely}; use style::parser::Parse; use style::values::specified::position::*; use style_traits::ToCss; @@ -168,3 +168,68 @@ fn test_grid_auto_flow() { assert!(parse(grid_auto_flow::parse, "column 'dense'").is_err()); assert!(parse(grid_auto_flow::parse, "column 2px dense").is_err()); } + +#[test] +fn test_grid_auto_rows_columns() { + use style::properties::longhands::grid_auto_rows; + + // the grammar is <track-size>+ but gecko supports only a single value, so we've clamped ourselves + assert_roundtrip_with_context!(grid_auto_rows::parse, "55%"); + assert_roundtrip_with_context!(grid_auto_rows::parse, "0.5fr"); + assert_roundtrip_with_context!(grid_auto_rows::parse, "fit-content(11%)"); + // only <inflexible-breadth> is allowed in first arg of minmax + assert!(parse(grid_auto_rows::parse, "minmax(1fr, max-content)").is_err()); +} + +#[test] +fn test_grid_template_rows_columns() { + use style::properties::longhands::grid_template_rows; + + assert_roundtrip_with_context!(grid_template_rows::parse, "none"); // none keyword + // <track-size>{2} with `<track-breadth> minmax(<inflexible-breadth>, <track-breadth>)` + assert_roundtrip_with_context!(grid_template_rows::parse, "1fr minmax(min-content, 1fr)"); + // <track-size> with <track-breadth> as <length-percentage> + assert_roundtrip_with_context!(grid_template_rows::parse, "calc(4em + 5px)"); + // <track-size> with <length> followed by <track-repeat> with `<track-size>{3}` (<flex>, auto, minmax) + assert_roundtrip_with_context!(grid_template_rows::parse, "10px repeat(2, 1fr auto minmax(200px, 1fr))"); + // <track-repeat> with `<track-size> <line-names>` followed by <track-size> + assert_roundtrip_with_context!(grid_template_rows::parse, "repeat(4, 10px [col-start] 250px [col-end]) 10px"); + // mixture of <track-size>, <track-repeat> and <line-names> + assert_roundtrip_with_context!(grid_template_rows::parse, + "[a] auto [b] minmax(min-content, 1fr) [b c d] repeat(2, [e] 40px) repeat(5, [f g] auto [h]) [i]"); + + // no span allowed in <line-names> + assert!(parse(grid_template_rows::parse, "[a span] 10px").is_err()); + // <track-list> needs at least one <track-size> | <track-repeat> + assert!(parse(grid_template_rows::parse, "[a b c]").is_err()); + // at least one argument of <fixed-size> should be a <fixed-breadth> (i.e., <length-percentage>) + assert!(parse(grid_template_rows::parse, "[a b] repeat(auto-fill, 50px) minmax(auto, 1fr)").is_err()); + // fit-content is not a <fixed-size> + assert!(parse(grid_template_rows::parse, "[a b] repeat(auto-fill, fit-content(20%))").is_err()); + // <auto-track-list> only allows <fixed-size> | <fixed-repeat> + assert!(parse(grid_template_rows::parse, "[a] repeat(2, auto) repeat(auto-fill, 10px)").is_err()); + // only <inflexible-breadth> allowed in <auto-track-repeat> + assert!(parse(grid_template_rows::parse, "[a] repeat(auto-fill, 1fr)").is_err()); + // <auto-track-repeat> is allowed only once + assert!(parse(grid_template_rows::parse, "[a] repeat(auto-fit, [b] 8px) [c] repeat(auto-fill, [c] 8px)").is_err()); +} + +#[test] +fn test_computed_grid_template_rows_colums() { + use style::properties::longhands::grid_template_rows; + + assert_computed_serialization(grid_template_rows::parse, + "[a] repeat(calc(1 + 1), [b] auto)", "[a b] auto [b] auto"); + + assert_computed_serialization(grid_template_rows::parse, + "[a] repeat(2, [b c] auto [e] auto [d])", + "[a b c] auto [e] auto [d b c] auto [e] auto [d]"); + + assert_computed_serialization(grid_template_rows::parse, + "[a] 50px [b] 10% [b c d] repeat(2, [e] 40px [f]) [g] repeat(auto-fill, [h i] 20px [j]) [k] 10px [l]", + "[a] 50px [b] 10% [b c d e] 40px [f e] 40px [f g] repeat(auto-fill, [h i] 20px [j]) [k] 10px [l]"); + + assert_computed_serialization(grid_template_rows::parse, + "10px repeat(2, 1fr auto minmax(200px, 1fr))", + "10px minmax(auto, 1fr) auto minmax(200px, 1fr) minmax(auto, 1fr) auto minmax(200px, 1fr)"); +} |