diff options
Diffstat (limited to 'servo/components/style/properties/shorthands')
15 files changed, 3718 insertions, 0 deletions
diff --git a/servo/components/style/properties/shorthands/background.mako.rs b/servo/components/style/properties/shorthands/background.mako.rs new file mode 100644 index 0000000000..075e78eb08 --- /dev/null +++ b/servo/components/style/properties/shorthands/background.mako.rs @@ -0,0 +1,289 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +// TODO: other background-* properties +<%helpers:shorthand name="background" + engines="gecko servo-2013 servo-2020" + sub_properties="background-color background-position-x background-position-y background-repeat + background-attachment background-image background-size background-origin + background-clip" + spec="https://drafts.csswg.org/css-backgrounds/#the-background"> + use crate::properties::longhands::{background_position_x, background_position_y, background_repeat}; + use crate::properties::longhands::{background_attachment, background_color, background_image, background_size, background_origin}; + use crate::properties::longhands::background_clip; + use crate::properties::longhands::background_clip::single_value::computed_value::T as Clip; + use crate::properties::longhands::background_origin::single_value::computed_value::T as Origin; + use crate::values::specified::{AllowQuirks, Color, Position, PositionComponent}; + use crate::parser::Parse; + + // FIXME(emilio): Should be the same type! + impl From<background_origin::single_value::SpecifiedValue> for background_clip::single_value::SpecifiedValue { + fn from(origin: background_origin::single_value::SpecifiedValue) -> + background_clip::single_value::SpecifiedValue { + match origin { + background_origin::single_value::SpecifiedValue::ContentBox => + background_clip::single_value::SpecifiedValue::ContentBox, + background_origin::single_value::SpecifiedValue::PaddingBox => + background_clip::single_value::SpecifiedValue::PaddingBox, + background_origin::single_value::SpecifiedValue::BorderBox => + background_clip::single_value::SpecifiedValue::BorderBox, + } + } + } + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let mut background_color = None; + + % for name in "image position_x position_y repeat size attachment origin clip".split(): + // Vec grows from 0 to 4 by default on first push(). So allocate with + // capacity 1, so in the common case of only one item we don't way + // overallocate, then shrink. Note that we always push at least one + // item if parsing succeeds. + let mut background_${name} = Vec::with_capacity(1); + % endfor + input.parse_comma_separated(|input| { + // background-color can only be in the last element, so if it + // is parsed anywhere before, the value is invalid. + if background_color.is_some() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + % for name in "image position repeat size attachment origin clip".split(): + let mut ${name} = None; + % endfor + loop { + if background_color.is_none() { + if let Ok(value) = input.try_parse(|i| Color::parse(context, i)) { + background_color = Some(value); + continue + } + } + if position.is_none() { + if let Ok(value) = input.try_parse(|input| { + Position::parse_three_value_quirky(context, input, AllowQuirks::No) + }) { + position = Some(value); + + // Parse background size, if applicable. + size = input.try_parse(|input| { + input.expect_delim('/')?; + background_size::single_value::parse(context, input) + }).ok(); + + continue + } + } + % for name in "image repeat attachment origin clip".split(): + if ${name}.is_none() { + if let Ok(value) = input.try_parse(|input| background_${name}::single_value + ::parse(context, input)) { + ${name} = Some(value); + continue + } + } + % endfor + break + } + if clip.is_none() { + if let Some(origin) = origin { + clip = Some(background_clip::single_value::SpecifiedValue::from(origin)); + } + } + let mut any = false; + % for name in "image position repeat size attachment origin clip".split(): + any = any || ${name}.is_some(); + % endfor + any = any || background_color.is_some(); + if any { + if let Some(position) = position { + background_position_x.push(position.horizontal); + background_position_y.push(position.vertical); + } else { + background_position_x.push(PositionComponent::zero()); + background_position_y.push(PositionComponent::zero()); + } + % for name in "image repeat size attachment origin clip".split(): + if let Some(bg_${name}) = ${name} { + background_${name}.push(bg_${name}); + } else { + background_${name}.push(background_${name}::single_value + ::get_initial_specified_value()); + } + % endfor + Ok(()) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + })?; + + Ok(expanded! { + background_color: background_color.unwrap_or(Color::transparent()), + % for name in "image position_x position_y repeat size attachment origin clip".split(): + background_${name}: background_${name}::SpecifiedValue(background_${name}.into()), + % endfor + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + let len = self.background_image.0.len(); + // There should be at least one declared value + if len == 0 { + return Ok(()); + } + + // If a value list length is differs then we don't do a shorthand serialization. + // The exceptions to this is color which appears once only and is serialized + // with the last item. + % for name in "image position_x position_y size repeat origin clip attachment".split(): + if len != self.background_${name}.0.len() { + return Ok(()); + } + % endfor + + for i in 0..len { + % for name in "image position_x position_y repeat size attachment origin clip".split(): + let ${name} = &self.background_${name}.0[i]; + % endfor + + if i != 0 { + dest.write_str(", ")?; + } + + let mut wrote_value = false; + + if i == len - 1 { + if *self.background_color != background_color::get_initial_specified_value() { + self.background_color.to_css(dest)?; + wrote_value = true; + } + } + + if *image != background_image::single_value::get_initial_specified_value() { + if wrote_value { + dest.write_str(" ")?; + } + image.to_css(dest)?; + wrote_value = true; + } + + // Size is only valid after a position so when there is a + // non-initial size we must also serialize position + if *position_x != PositionComponent::zero() || + *position_y != PositionComponent::zero() || + *size != background_size::single_value::get_initial_specified_value() + { + if wrote_value { + dest.write_str(" ")?; + } + + Position { + horizontal: position_x.clone(), + vertical: position_y.clone() + }.to_css(dest)?; + + wrote_value = true; + + if *size != background_size::single_value::get_initial_specified_value() { + dest.write_str(" / ")?; + size.to_css(dest)?; + } + } + + % for name in "repeat attachment".split(): + if *${name} != background_${name}::single_value::get_initial_specified_value() { + if wrote_value { + dest.write_str(" ")?; + } + ${name}.to_css(dest)?; + wrote_value = true; + } + % endfor + + if *origin != Origin::PaddingBox || *clip != Clip::BorderBox { + if wrote_value { + dest.write_str(" ")?; + } + origin.to_css(dest)?; + if *clip != From::from(*origin) { + dest.write_str(" ")?; + clip.to_css(dest)?; + } + + wrote_value = true; + } + + if !wrote_value { + image.to_css(dest)?; + } + } + + Ok(()) + } + } +</%helpers:shorthand> + +<%helpers:shorthand name="background-position" + engines="gecko servo-2013 servo-2020" + flags="SHORTHAND_IN_GETCS" + sub_properties="background-position-x background-position-y" + spec="https://drafts.csswg.org/css-backgrounds-4/#the-background-position"> + use crate::properties::longhands::{background_position_x, background_position_y}; + use crate::values::specified::AllowQuirks; + use crate::values::specified::position::Position; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + // Vec grows from 0 to 4 by default on first push(). So allocate with + // capacity 1, so in the common case of only one item we don't way + // overallocate, then shrink. Note that we always push at least one + // item if parsing succeeds. + let mut position_x = Vec::with_capacity(1); + let mut position_y = Vec::with_capacity(1); + let mut any = false; + + input.parse_comma_separated(|input| { + let value = Position::parse_three_value_quirky(context, input, AllowQuirks::Yes)?; + position_x.push(value.horizontal); + position_y.push(value.vertical); + any = true; + Ok(()) + })?; + if !any { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(expanded! { + background_position_x: background_position_x::SpecifiedValue(position_x.into()), + background_position_y: background_position_y::SpecifiedValue(position_y.into()), + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + let len = self.background_position_x.0.len(); + if len == 0 || len != self.background_position_y.0.len() { + return Ok(()); + } + for i in 0..len { + Position { + horizontal: self.background_position_x.0[i].clone(), + vertical: self.background_position_y.0[i].clone() + }.to_css(dest)?; + + if i < len - 1 { + dest.write_str(", ")?; + } + } + Ok(()) + } + } +</%helpers:shorthand> diff --git a/servo/components/style/properties/shorthands/border.mako.rs b/servo/components/style/properties/shorthands/border.mako.rs new file mode 100644 index 0000000000..b8c5a7f2e1 --- /dev/null +++ b/servo/components/style/properties/shorthands/border.mako.rs @@ -0,0 +1,479 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> +<% from data import to_rust_ident, ALL_SIDES, PHYSICAL_SIDES, maybe_moz_logical_alias %> + +${helpers.four_sides_shorthand( + "border-color", + "border-%s-color", + "specified::Color::parse", + engines="gecko servo-2013 servo-2020", + spec="https://drafts.csswg.org/css-backgrounds/#border-color", + allow_quirks="Yes", +)} + +${helpers.four_sides_shorthand( + "border-style", + "border-%s-style", + engines="gecko servo-2013 servo-2020", + spec="https://drafts.csswg.org/css-backgrounds/#border-style", +)} + +<%helpers:shorthand + name="border-width" + engines="gecko servo-2013 servo-2020" + sub_properties="${ + ' '.join('border-%s-width' % side + for side in PHYSICAL_SIDES)}" + spec="https://drafts.csswg.org/css-backgrounds/#border-width"> + use crate::values::generics::rect::Rect; + use crate::values::specified::{AllowQuirks, BorderSideWidth}; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let rect = Rect::parse_with(context, input, |_, i| { + BorderSideWidth::parse_quirky(context, i, AllowQuirks::Yes) + })?; + Ok(expanded! { + border_top_width: rect.0, + border_right_width: rect.1, + border_bottom_width: rect.2, + border_left_width: rect.3, + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + % for side in PHYSICAL_SIDES: + let ${side} = &self.border_${side}_width; + % endfor + Rect::new(top, right, bottom, left).to_css(dest) + } + } +</%helpers:shorthand> + + +pub fn parse_border<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, +) -> Result<(specified::Color, specified::BorderStyle, specified::BorderSideWidth), ParseError<'i>> { + use crate::values::specified::{Color, BorderStyle, BorderSideWidth}; + let _unused = context; + let mut color = None; + let mut style = None; + let mut width = None; + let mut any = false; + loop { + if width.is_none() { + if let Ok(value) = input.try_parse(|i| BorderSideWidth::parse(context, i)) { + width = Some(value); + any = true; + } + } + if style.is_none() { + if let Ok(value) = input.try_parse(BorderStyle::parse) { + style = Some(value); + any = true; + continue + } + } + if color.is_none() { + if let Ok(value) = input.try_parse(|i| Color::parse(context, i)) { + color = Some(value); + any = true; + continue + } + } + break + } + if any { + Ok((color.unwrap_or_else(|| Color::currentcolor()), + style.unwrap_or(BorderStyle::None), + width.unwrap_or(BorderSideWidth::Medium))) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } +} + +% for side, logical in ALL_SIDES: + <% + spec = "https://drafts.csswg.org/css-backgrounds/#border-%s" % side + if logical: + spec = "https://drafts.csswg.org/css-logical-props/#propdef-border-%s" % side + %> + <%helpers:shorthand + name="border-${side}" + engines="gecko servo-2013 servo-2020" + sub_properties="${' '.join( + 'border-%s-%s' % (side, prop) + for prop in ['color', 'style', 'width'] + )}" + aliases="${maybe_moz_logical_alias(engine, (side, logical), '-moz-border-%s')}" + spec="${spec}"> + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let (color, style, width) = super::parse_border(context, input)?; + Ok(expanded! { + border_${to_rust_ident(side)}_color: color, + border_${to_rust_ident(side)}_style: style, + border_${to_rust_ident(side)}_width: width + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + super::serialize_directional_border( + dest, + self.border_${to_rust_ident(side)}_width, + self.border_${to_rust_ident(side)}_style, + self.border_${to_rust_ident(side)}_color + ) + } + } + + </%helpers:shorthand> +% endfor + +<%helpers:shorthand name="border" + engines="gecko servo-2013 servo-2020" + sub_properties="${' '.join('border-%s-%s' % (side, prop) + for side in PHYSICAL_SIDES + for prop in ['color', 'style', 'width'])} + ${' '.join('border-image-%s' % name + for name in ['outset', 'repeat', 'slice', 'source', 'width'])}" + derive_value_info="False" + spec="https://drafts.csswg.org/css-backgrounds/#border"> + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + use crate::properties::longhands::{border_image_outset, border_image_repeat, border_image_slice}; + use crate::properties::longhands::{border_image_source, border_image_width}; + + let (color, style, width) = super::parse_border(context, input)?; + Ok(expanded! { + % for side in PHYSICAL_SIDES: + border_${side}_color: color.clone(), + border_${side}_style: style, + border_${side}_width: width.clone(), + % endfor + + // The ‘border’ shorthand resets ‘border-image’ to its initial value. + // See https://drafts.csswg.org/css-backgrounds-3/#the-border-shorthands + % for name in "outset repeat slice source width".split(): + border_image_${name}: border_image_${name}::get_initial_specified_value(), + % endfor + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + use crate::properties::longhands; + + // If any of the border-image longhands differ from their initial specified values we should not + // invoke serialize_directional_border(), so there is no point in continuing on to compute all_equal. + % for name in "outset repeat slice source width".split(): + if *self.border_image_${name} != longhands::border_image_${name}::get_initial_specified_value() { + return Ok(()); + } + % endfor + + let all_equal = { + % for side in PHYSICAL_SIDES: + let border_${side}_width = self.border_${side}_width; + let border_${side}_style = self.border_${side}_style; + let border_${side}_color = self.border_${side}_color; + % endfor + + border_top_width == border_right_width && + border_right_width == border_bottom_width && + border_bottom_width == border_left_width && + + border_top_style == border_right_style && + border_right_style == border_bottom_style && + border_bottom_style == border_left_style && + + border_top_color == border_right_color && + border_right_color == border_bottom_color && + border_bottom_color == border_left_color + }; + + // If all longhands are all present, then all sides should be the same, + // so we can just one set of color/style/width + if all_equal { + super::serialize_directional_border( + dest, + self.border_${side}_width, + self.border_${side}_style, + self.border_${side}_color + ) + } else { + Ok(()) + } + } + } + + // Just use the same as border-left. The border shorthand can't accept + // any value that the sub-shorthand couldn't. + <% + border_left = "<crate::properties::shorthands::border_left::Longhands as SpecifiedValueInfo>" + %> + impl SpecifiedValueInfo for Longhands { + const SUPPORTED_TYPES: u8 = ${border_left}::SUPPORTED_TYPES; + fn collect_completion_keywords(f: KeywordsCollectFn) { + ${border_left}::collect_completion_keywords(f); + } + } +</%helpers:shorthand> + +<%helpers:shorthand + name="border-radius" + engines="gecko servo-2013 servo-2020" + sub_properties="${' '.join( + 'border-%s-radius' % (corner) + for corner in ['top-left', 'top-right', 'bottom-right', 'bottom-left'] + )}" + extra_prefixes="webkit" + spec="https://drafts.csswg.org/css-backgrounds/#border-radius" +> + use crate::values::generics::rect::Rect; + use crate::values::generics::border::BorderCornerRadius; + use crate::values::specified::border::BorderRadius; + use crate::parser::Parse; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let radii = BorderRadius::parse(context, input)?; + Ok(expanded! { + border_top_left_radius: radii.top_left, + border_top_right_radius: radii.top_right, + border_bottom_right_radius: radii.bottom_right, + border_bottom_left_radius: radii.bottom_left, + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + let LonghandsToSerialize { + border_top_left_radius: &BorderCornerRadius(ref tl), + border_top_right_radius: &BorderCornerRadius(ref tr), + border_bottom_right_radius: &BorderCornerRadius(ref br), + border_bottom_left_radius: &BorderCornerRadius(ref bl), + } = *self; + + + let widths = Rect::new(tl.width(), tr.width(), br.width(), bl.width()); + let heights = Rect::new(tl.height(), tr.height(), br.height(), bl.height()); + + BorderRadius::serialize_rects(widths, heights, dest) + } + } +</%helpers:shorthand> + +<%helpers:shorthand + name="border-image" + engines="gecko servo-2013" + sub_properties="border-image-outset + border-image-repeat border-image-slice border-image-source border-image-width" + extra_prefixes="moz:layout.css.prefixes.border-image webkit" + spec="https://drafts.csswg.org/css-backgrounds-3/#border-image" +> + use crate::properties::longhands::{border_image_outset, border_image_repeat, border_image_slice}; + use crate::properties::longhands::{border_image_source, border_image_width}; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + % for name in "outset repeat slice source width".split(): + let mut border_image_${name} = border_image_${name}::get_initial_specified_value(); + % endfor + + let result: Result<_, ParseError> = input.try_parse(|input| { + % for name in "outset repeat slice source width".split(): + let mut ${name} = None; + % endfor + loop { + if slice.is_none() { + if let Ok(value) = input.try_parse(|input| border_image_slice::parse(context, input)) { + slice = Some(value); + // Parse border image width and outset, if applicable. + let maybe_width_outset: Result<_, ParseError> = input.try_parse(|input| { + input.expect_delim('/')?; + + // Parse border image width, if applicable. + let w = input.try_parse(|input| + border_image_width::parse(context, input)).ok(); + + // Parse border image outset if applicable. + let o = input.try_parse(|input| { + input.expect_delim('/')?; + border_image_outset::parse(context, input) + }).ok(); + if w.is_none() && o.is_none() { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + else { + Ok((w, o)) + } + }); + if let Ok((w, o)) = maybe_width_outset { + width = w; + outset = o; + } + + continue + } + } + % for name in "source repeat".split(): + if ${name}.is_none() { + if let Ok(value) = input.try_parse(|input| border_image_${name}::parse(context, input)) { + ${name} = Some(value); + continue + } + } + % endfor + break + } + let mut any = false; + % for name in "outset repeat slice source width".split(): + any = any || ${name}.is_some(); + % endfor + if any { + % for name in "outset repeat slice source width".split(): + if let Some(b_${name}) = ${name} { + border_image_${name} = b_${name}; + } + % endfor + Ok(()) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + }); + result?; + + Ok(expanded! { + % for name in "outset repeat slice source width".split(): + border_image_${name}: border_image_${name}, + % endfor + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + self.border_image_source.to_css(dest)?; + dest.write_str(" ")?; + self.border_image_slice.to_css(dest)?; + dest.write_str(" / ")?; + self.border_image_width.to_css(dest)?; + dest.write_str(" / ")?; + self.border_image_outset.to_css(dest)?; + dest.write_str(" ")?; + self.border_image_repeat.to_css(dest) + } + } +</%helpers:shorthand> + +% for axis in ["block", "inline"]: + % for prop in ["width", "style", "color"]: + <% + spec = "https://drafts.csswg.org/css-logical/#propdef-border-%s-%s" % (axis, prop) + %> + <%helpers:shorthand + engines="gecko servo-2013 servo-2020" + name="border-${axis}-${prop}" + sub_properties="${' '.join( + 'border-%s-%s-%s' % (axis, side, prop) + for side in ['start', 'end'] + )}" + spec="${spec}"> + + use crate::properties::longhands::border_${axis}_start_${prop}; + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let start_value = border_${axis}_start_${prop}::parse(context, input)?; + let end_value = + input.try_parse(|input| border_${axis}_start_${prop}::parse(context, input)) + .unwrap_or_else(|_| start_value.clone()); + + Ok(expanded! { + border_${axis}_start_${prop}: start_value, + border_${axis}_end_${prop}: end_value, + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + self.border_${axis}_start_${prop}.to_css(dest)?; + + if self.border_${axis}_end_${prop} != self.border_${axis}_start_${prop} { + dest.write_str(" ")?; + self.border_${axis}_end_${prop}.to_css(dest)?; + } + + Ok(()) + } + } + </%helpers:shorthand> + % endfor +% endfor + +% for axis in ["block", "inline"]: + <% + spec = "https://drafts.csswg.org/css-logical/#propdef-border-%s" % (axis) + %> + <%helpers:shorthand + name="border-${axis}" + engines="gecko servo-2013 servo-2020" + sub_properties="${' '.join( + 'border-%s-%s-width' % (axis, side) + for side in ['start', 'end'] + )} ${' '.join( + 'border-%s-%s-style' % (axis, side) + for side in ['start', 'end'] + )} ${' '.join( + 'border-%s-%s-color' % (axis, side) + for side in ['start', 'end'] + )}" + spec="${spec}"> + + use crate::properties::shorthands::border_${axis}_start; + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let start_value = border_${axis}_start::parse_value(context, input)?; + Ok(expanded! { + border_${axis}_start_width: start_value.border_${axis}_start_width.clone(), + border_${axis}_end_width: start_value.border_${axis}_start_width, + border_${axis}_start_style: start_value.border_${axis}_start_style.clone(), + border_${axis}_end_style: start_value.border_${axis}_start_style, + border_${axis}_start_color: start_value.border_${axis}_start_color.clone(), + border_${axis}_end_color: start_value.border_${axis}_start_color, + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + super::serialize_directional_border( + dest, + self.border_${axis}_start_width, + self.border_${axis}_start_style, + self.border_${axis}_start_color + ) + } + } + </%helpers:shorthand> +% endfor diff --git a/servo/components/style/properties/shorthands/box.mako.rs b/servo/components/style/properties/shorthands/box.mako.rs new file mode 100644 index 0000000000..fc4703e829 --- /dev/null +++ b/servo/components/style/properties/shorthands/box.mako.rs @@ -0,0 +1,279 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +${helpers.two_properties_shorthand( + "overflow", + "overflow-x", + "overflow-y", + engines="gecko servo-2013 servo-2020", + flags="SHORTHAND_IN_GETCS", + spec="https://drafts.csswg.org/css-overflow/#propdef-overflow", +)} + +${helpers.two_properties_shorthand( + "overflow-clip-box", + "overflow-clip-box-block", + "overflow-clip-box-inline", + engines="gecko", + enabled_in="ua", + gecko_pref="layout.css.overflow-clip-box.enabled", + spec="Internal, may be standardized in the future " + "(https://developer.mozilla.org/en-US/docs/Web/CSS/overflow-clip-box)", +)} + +${helpers.two_properties_shorthand( + "overscroll-behavior", + "overscroll-behavior-x", + "overscroll-behavior-y", + engines="gecko", + gecko_pref="layout.css.overscroll-behavior.enabled", + spec="https://wicg.github.io/overscroll-behavior/#overscroll-behavior-properties", +)} + +<%helpers:shorthand + engines="gecko" + name="container" + sub_properties="container-name container-type" + gecko_pref="layout.css.container-queries.enabled" + enabled_in="ua" + spec="https://drafts.csswg.org/css-contain-3/#container-shorthand" +> + use crate::values::specified::box_::{ContainerName, ContainerType}; + pub fn parse_value<'i>( + context: &ParserContext, + input: &mut Parser<'i, '_>, + ) -> Result<Longhands, ParseError<'i>> { + use crate::parser::Parse; + // See https://github.com/w3c/csswg-drafts/issues/7180 for why we don't + // match the spec. + let container_name = ContainerName::parse(context, input)?; + let container_type = if input.try_parse(|input| input.expect_delim('/')).is_ok() { + ContainerType::parse(input)? + } else { + ContainerType::Normal + }; + Ok(expanded! { + container_name: container_name, + container_type: container_type, + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + self.container_name.to_css(dest)?; + if !self.container_type.is_normal() { + dest.write_str(" / ")?; + self.container_type.to_css(dest)?; + } + Ok(()) + } + } +</%helpers:shorthand> + +<%helpers:shorthand + engines="gecko" + name="page-break-before" + flags="SHORTHAND_IN_GETCS IS_LEGACY_SHORTHAND" + sub_properties="break-before" + spec="https://drafts.csswg.org/css-break-3/#page-break-properties" +> + pub fn parse_value<'i>( + context: &ParserContext, + input: &mut Parser<'i, '_>, + ) -> Result<Longhands, ParseError<'i>> { + use crate::values::specified::box_::BreakBetween; + Ok(expanded! { + break_before: BreakBetween::parse_legacy(context, input)?, + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + self.break_before.to_css_legacy(dest) + } + } +</%helpers:shorthand> + +<%helpers:shorthand + engines="gecko" + name="page-break-after" + flags="SHORTHAND_IN_GETCS IS_LEGACY_SHORTHAND" + sub_properties="break-after" + spec="https://drafts.csswg.org/css-break-3/#page-break-properties" +> + pub fn parse_value<'i>( + context: &ParserContext, + input: &mut Parser<'i, '_>, + ) -> Result<Longhands, ParseError<'i>> { + use crate::values::specified::box_::BreakBetween; + Ok(expanded! { + break_after: BreakBetween::parse_legacy(context, input)?, + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + self.break_after.to_css_legacy(dest) + } + } +</%helpers:shorthand> + +<%helpers:shorthand + engines="gecko" + name="page-break-inside" + flags="SHORTHAND_IN_GETCS IS_LEGACY_SHORTHAND" + sub_properties="break-inside" + spec="https://drafts.csswg.org/css-break-3/#page-break-properties" +> + pub fn parse_value<'i>( + context: &ParserContext, + input: &mut Parser<'i, '_>, + ) -> Result<Longhands, ParseError<'i>> { + use crate::values::specified::box_::BreakWithin; + Ok(expanded! { + break_inside: BreakWithin::parse_legacy(context, input)?, + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + self.break_inside.to_css_legacy(dest) + } + } +</%helpers:shorthand> + +<%helpers:shorthand name="offset" + engines="gecko" + sub_properties="offset-path offset-distance offset-rotate offset-anchor" + gecko_pref="layout.css.motion-path.enabled", + spec="https://drafts.fxtf.org/motion-1/#offset-shorthand"> + use crate::parser::Parse; + use crate::values::specified::motion::{OffsetPath, OffsetRotate}; + use crate::values::specified::position::PositionOrAuto; + use crate::values::specified::LengthPercentage; + use crate::Zero; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + // FIXME: Bug 1559232: Support offset-position. + // Per the spec, this must have offet-position and/or offset-path. However, we don't + // support offset-position, so offset-path is necessary now. + let offset_path = OffsetPath::parse(context, input)?; + + let mut offset_distance = None; + let mut offset_rotate = None; + loop { + if offset_distance.is_none() { + if let Ok(value) = input.try_parse(|i| LengthPercentage::parse(context, i)) { + offset_distance = Some(value); + } + } + + if offset_rotate.is_none() { + if let Ok(value) = input.try_parse(|i| OffsetRotate::parse(context, i)) { + offset_rotate = Some(value); + continue; + } + } + break; + } + + let offset_anchor = input.try_parse(|i| { + i.expect_delim('/')?; + PositionOrAuto::parse(context, i) + }).ok(); + + Ok(expanded! { + offset_path: offset_path, + offset_distance: offset_distance.unwrap_or(LengthPercentage::zero()), + offset_rotate: offset_rotate.unwrap_or(OffsetRotate::auto()), + offset_anchor: offset_anchor.unwrap_or(PositionOrAuto::auto()), + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + // FIXME: Bug 1559232: Support offset-position. We don't support offset-position, + // so always serialize offset-path now. + self.offset_path.to_css(dest)?; + + if !self.offset_distance.is_zero() { + dest.write_str(" ")?; + self.offset_distance.to_css(dest)?; + } + + if !self.offset_rotate.is_auto() { + dest.write_str(" ")?; + self.offset_rotate.to_css(dest)?; + } + + if *self.offset_anchor != PositionOrAuto::auto() { + dest.write_str(" / ")?; + self.offset_anchor.to_css(dest)?; + } + Ok(()) + } + } +</%helpers:shorthand> + +<%helpers:shorthand name="zoom" engines="gecko" + sub_properties="transform transform-origin" + gecko_pref="layout.css.zoom-transform-hack.enabled" + flags="SHORTHAND_IN_GETCS IS_LEGACY_SHORTHAND" + spec="Not a standard, only a compat hack"> + use crate::parser::Parse; + use crate::values::specified::{Number, NumberOrPercentage, TransformOrigin}; + use crate::values::generics::transform::{Transform, TransformOperation}; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let zoom = match input.try_parse(|input| NumberOrPercentage::parse(context, input)) { + Ok(number_or_percent) => number_or_percent.to_number(), + Err(..) => { + input.expect_ident_matching("normal")?; + Number::new(1.0) + }, + }; + + // Make sure that the initial value matches the values for the + // longhands, just for general sanity. `zoom: 1` and `zoom: 0` are + // ignored, see [1][2]. They are just hack for the "has layout" mode on + // IE. + // + // [1]: https://bugs.webkit.org/show_bug.cgi?id=18467 + // [2]: https://bugzilla.mozilla.org/show_bug.cgi?id=1593009 + Ok(if zoom.get() == 1.0 || zoom.get() == 0.0 { + expanded! { + transform: Transform::none(), + transform_origin: TransformOrigin::initial_value(), + } + } else { + expanded! { + transform: Transform(vec![TransformOperation::Scale(zoom, zoom)].into()), + transform_origin: TransformOrigin::zero_zero(), + } + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + if self.transform.0.is_empty() && *self.transform_origin == TransformOrigin::initial_value() { + return 1.0f32.to_css(dest); + } + if *self.transform_origin != TransformOrigin::zero_zero() { + return Ok(()) + } + match &*self.transform.0 { + [TransformOperation::Scale(x, y)] if x == y => x.to_css(dest), + _ => Ok(()), + } + } + } +</%helpers:shorthand> diff --git a/servo/components/style/properties/shorthands/column.mako.rs b/servo/components/style/properties/shorthands/column.mako.rs new file mode 100644 index 0000000000..3740775a7e --- /dev/null +++ b/servo/components/style/properties/shorthands/column.mako.rs @@ -0,0 +1,115 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +<%helpers:shorthand name="columns" + engines="gecko servo-2013" + sub_properties="column-width column-count" + servo_2013_pref="layout.columns.enabled", + spec="https://drafts.csswg.org/css-multicol/#propdef-columns"> + use crate::properties::longhands::{column_count, column_width}; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let mut column_count = None; + let mut column_width = None; + let mut autos = 0; + + loop { + if input.try_parse(|input| input.expect_ident_matching("auto")).is_ok() { + // Leave the options to None, 'auto' is the initial value. + autos += 1; + continue + } + + if column_count.is_none() { + if let Ok(value) = input.try_parse(|input| column_count::parse(context, input)) { + column_count = Some(value); + continue + } + } + + if column_width.is_none() { + if let Ok(value) = input.try_parse(|input| column_width::parse(context, input)) { + column_width = Some(value); + continue + } + } + + break + } + + let values = autos + column_count.iter().len() + column_width.iter().len(); + if values == 0 || values > 2 { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } else { + Ok(expanded! { + column_count: unwrap_or_initial!(column_count), + column_width: unwrap_or_initial!(column_width), + }) + } + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + if self.column_width.is_auto() { + return self.column_count.to_css(dest) + } + self.column_width.to_css(dest)?; + if !self.column_count.is_auto() { + dest.write_char(' ')?; + self.column_count.to_css(dest)?; + } + Ok(()) + } + } +</%helpers:shorthand> + +<%helpers:shorthand + name="column-rule" + engines="gecko" + sub_properties="column-rule-width column-rule-style column-rule-color" + derive_serialize="True" + spec="https://drafts.csswg.org/css-multicol/#propdef-column-rule" +> + use crate::properties::longhands::{column_rule_width, column_rule_style}; + use crate::properties::longhands::column_rule_color; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + % for name in "width style color".split(): + let mut column_rule_${name} = None; + % endfor + let mut any = false; + + loop { + % for name in "width style color".split(): + if column_rule_${name}.is_none() { + if let Ok(value) = input.try_parse(|input| + column_rule_${name}::parse(context, input)) { + column_rule_${name} = Some(value); + any = true; + continue + } + } + % endfor + + break + } + if any { + Ok(expanded! { + column_rule_width: unwrap_or_initial!(column_rule_width), + column_rule_style: unwrap_or_initial!(column_rule_style), + column_rule_color: unwrap_or_initial!(column_rule_color), + }) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +</%helpers:shorthand> diff --git a/servo/components/style/properties/shorthands/font.mako.rs b/servo/components/style/properties/shorthands/font.mako.rs new file mode 100644 index 0000000000..7e5154676b --- /dev/null +++ b/servo/components/style/properties/shorthands/font.mako.rs @@ -0,0 +1,453 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> +<% from data import SYSTEM_FONT_LONGHANDS %> + +<%helpers:shorthand + name="font" + engines="gecko servo-2013 servo-2020" + sub_properties=" + font-style + font-variant-caps + font-weight + font-stretch + font-size + line-height + font-family + ${'font-size-adjust' if engine == 'gecko' else ''} + ${'font-kerning' if engine == 'gecko' else ''} + ${'font-optical-sizing' if engine == 'gecko' else ''} + ${'font-variant-alternates' if engine == 'gecko' else ''} + ${'font-variant-east-asian' if engine == 'gecko' else ''} + ${'font-variant-emoji' if engine == 'gecko' else ''} + ${'font-variant-ligatures' if engine == 'gecko' else ''} + ${'font-variant-numeric' if engine == 'gecko' else ''} + ${'font-variant-position' if engine == 'gecko' else ''} + ${'font-language-override' if engine == 'gecko' else ''} + ${'font-feature-settings' if engine == 'gecko' else ''} + ${'font-variation-settings' if engine == 'gecko' else ''} + " + derive_value_info="False" + spec="https://drafts.csswg.org/css-fonts-3/#propdef-font" +> + use crate::computed_values::font_variant_caps::T::SmallCaps; + use crate::parser::Parse; + use crate::properties::longhands::{font_family, font_style, font_size, font_weight, font_stretch}; + use crate::properties::longhands::font_variant_caps; + use crate::values::specified::text::LineHeight; + use crate::values::specified::FontSize; + use crate::values::specified::font::{FontStretch, FontStretchKeyword}; + #[cfg(feature = "gecko")] + use crate::values::specified::font::SystemFont; + + <% + gecko_sub_properties = "kerning language_override size_adjust \ + variant_alternates variant_east_asian \ + variant_emoji variant_ligatures \ + variant_numeric variant_position \ + feature_settings variation_settings \ + optical_sizing".split() + %> + % if engine == "gecko": + % for prop in gecko_sub_properties: + use crate::properties::longhands::font_${prop}; + % endfor + % endif + use self::font_family::SpecifiedValue as FontFamily; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let mut nb_normals = 0; + let mut style = None; + let mut variant_caps = None; + let mut weight = None; + let mut stretch = None; + let size; + % if engine == "gecko": + if let Ok(sys) = input.try_parse(|i| SystemFont::parse(context, i)) { + return Ok(expanded! { + % for name in SYSTEM_FONT_LONGHANDS: + ${name}: ${name}::SpecifiedValue::system_font(sys), + % endfor + line_height: LineHeight::normal(), + % for name in gecko_sub_properties + ["variant_caps"]: + font_${name}: font_${name}::get_initial_specified_value(), + % endfor + }) + } + % endif + loop { + // Special-case 'normal' because it is valid in each of + // font-style, font-weight, font-variant and font-stretch. + // Leaves the values to None, 'normal' is the initial value for each of them. + if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() { + nb_normals += 1; + continue; + } + if style.is_none() { + if let Ok(value) = input.try_parse(|input| font_style::parse(context, input)) { + style = Some(value); + continue + } + } + if weight.is_none() { + if let Ok(value) = input.try_parse(|input| font_weight::parse(context, input)) { + weight = Some(value); + continue + } + } + if variant_caps.is_none() { + // The only variant-caps value allowed is small-caps (from CSS2); the added values + // defined by CSS Fonts 3 and later are not accepted. + // https://www.w3.org/TR/css-fonts-4/#font-prop + if input.try_parse(|input| input.expect_ident_matching("small-caps")).is_ok() { + variant_caps = Some(SmallCaps); + continue + } + } + if stretch.is_none() { + if let Ok(value) = input.try_parse(FontStretchKeyword::parse) { + stretch = Some(FontStretch::Keyword(value)); + continue + } + } + size = Some(FontSize::parse(context, input)?); + break + } + + let size = match size { + Some(s) => s, + None => { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + }; + + let line_height = if input.try_parse(|input| input.expect_delim('/')).is_ok() { + Some(LineHeight::parse(context, input)?) + } else { + None + }; + + #[inline] + fn count<T>(opt: &Option<T>) -> u8 { + if opt.is_some() { 1 } else { 0 } + } + + if (count(&style) + count(&weight) + count(&variant_caps) + count(&stretch) + nb_normals) > 4 { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + let family = FontFamily::parse(context, input)?; + Ok(expanded! { + % for name in "style weight stretch variant_caps".split(): + font_${name}: unwrap_or_initial!(font_${name}, ${name}), + % endfor + font_size: size, + line_height: line_height.unwrap_or(LineHeight::normal()), + font_family: family, + % if engine == "gecko": + % for name in gecko_sub_properties: + font_${name}: font_${name}::get_initial_specified_value(), + % endfor + % endif + }) + } + + % if engine == "gecko": + enum CheckSystemResult { + AllSystem(SystemFont), + SomeSystem, + None + } + % endif + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + % if engine == "gecko": + match self.check_system() { + CheckSystemResult::AllSystem(sys) => return sys.to_css(dest), + CheckSystemResult::SomeSystem => return Ok(()), + CheckSystemResult::None => {} + } + % endif + + % if engine == "gecko": + if let Some(v) = self.font_optical_sizing { + if v != &font_optical_sizing::get_initial_specified_value() { + return Ok(()); + } + } + if let Some(v) = self.font_variation_settings { + if v != &font_variation_settings::get_initial_specified_value() { + return Ok(()); + } + } + if let Some(v) = self.font_variant_emoji { + if v != &font_variant_emoji::get_initial_specified_value() { + return Ok(()); + } + } + + % for name in gecko_sub_properties: + % if name != "optical_sizing" and name != "variation_settings" and name != "variant_emoji": + if self.font_${name} != &font_${name}::get_initial_specified_value() { + return Ok(()); + } + % endif + % endfor + % endif + + // Only font-stretch keywords are allowed as part as the font + // shorthand. + let font_stretch = match *self.font_stretch { + FontStretch::Keyword(kw) => kw, + FontStretch::Stretch(percentage) => { + match FontStretchKeyword::from_percentage(percentage.0.get()) { + Some(kw) => kw, + None => return Ok(()), + } + } + FontStretch::System(..) => return Ok(()), + }; + + // The only variant-caps value allowed in the shorthand is small-caps (from CSS2); + // the added values defined by CSS Fonts 3 and later are not supported. + // https://www.w3.org/TR/css-fonts-4/#font-prop + if self.font_variant_caps != &font_variant_caps::get_initial_specified_value() && + *self.font_variant_caps != SmallCaps { + return Ok(()); + } + + % for name in "style variant_caps weight".split(): + if self.font_${name} != &font_${name}::get_initial_specified_value() { + self.font_${name}.to_css(dest)?; + dest.write_str(" ")?; + } + % endfor + + if font_stretch != FontStretchKeyword::Normal { + font_stretch.to_css(dest)?; + dest.write_str(" ")?; + } + + self.font_size.to_css(dest)?; + + if *self.line_height != LineHeight::normal() { + dest.write_str(" / ")?; + self.line_height.to_css(dest)?; + } + + dest.write_str(" ")?; + self.font_family.to_css(dest)?; + + Ok(()) + } + } + + impl<'a> LonghandsToSerialize<'a> { + % if engine == "gecko": + /// Check if some or all members are system fonts + fn check_system(&self) -> CheckSystemResult { + let mut sys = None; + let mut all = true; + + % for prop in SYSTEM_FONT_LONGHANDS: + % if prop == "font_optical_sizing" or prop == "font_variation_settings": + if let Some(value) = self.${prop} { + % else: + { + let value = self.${prop}; + % endif + match value.get_system() { + Some(s) => { + debug_assert!(sys.is_none() || s == sys.unwrap()); + sys = Some(s); + } + None => { + all = false; + } + } + } + % endfor + if self.line_height != &LineHeight::normal() { + all = false + } + if all { + CheckSystemResult::AllSystem(sys.unwrap()) + } else if sys.is_some() { + CheckSystemResult::SomeSystem + } else { + CheckSystemResult::None + } + } + % endif + } + + <% + subprops_for_value_info = ["font_style", "font_weight", "font_stretch", + "font_variant_caps", "font_size", "font_family"] + subprops_for_value_info = [ + "<longhands::{}::SpecifiedValue as SpecifiedValueInfo>".format(p) + for p in subprops_for_value_info + ] + %> + impl SpecifiedValueInfo for Longhands { + const SUPPORTED_TYPES: u8 = 0 + % for p in subprops_for_value_info: + | ${p}::SUPPORTED_TYPES + % endfor + ; + + fn collect_completion_keywords(f: KeywordsCollectFn) { + % for p in subprops_for_value_info: + ${p}::collect_completion_keywords(f); + % endfor + <SystemFont as SpecifiedValueInfo>::collect_completion_keywords(f); + } + } +</%helpers:shorthand> + +<%helpers:shorthand name="font-variant" + engines="gecko servo-2013" + flags="SHORTHAND_IN_GETCS" + sub_properties="font-variant-caps + ${'font-variant-alternates' if engine == 'gecko' else ''} + ${'font-variant-east-asian' if engine == 'gecko' else ''} + ${'font-variant-emoji' if engine == 'gecko' else ''} + ${'font-variant-ligatures' if engine == 'gecko' else ''} + ${'font-variant-numeric' if engine == 'gecko' else ''} + ${'font-variant-position' if engine == 'gecko' else ''}" + spec="https://drafts.csswg.org/css-fonts-3/#propdef-font-variant"> + <% gecko_sub_properties = "alternates east_asian emoji ligatures numeric position".split() %> + <% + sub_properties = ["caps"] + if engine == "gecko": + sub_properties += gecko_sub_properties + %> + +% for prop in sub_properties: + use crate::properties::longhands::font_variant_${prop}; +% endfor + #[allow(unused_imports)] + use crate::values::specified::FontVariantLigatures; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + % for prop in sub_properties: + let mut ${prop} = None; + % endfor + + if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() { + // Leave the values to None, 'normal' is the initial value for all the sub properties. + } else if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() { + // The 'none' value sets 'font-variant-ligatures' to 'none' and resets all other sub properties + // to their initial value. + % if engine == "gecko": + ligatures = Some(FontVariantLigatures::NONE); + % endif + } else { + let mut has_custom_value: bool = false; + loop { + if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() || + input.try_parse(|input| input.expect_ident_matching("none")).is_ok() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + % for prop in sub_properties: + if ${prop}.is_none() { + if let Ok(value) = input.try_parse(|i| font_variant_${prop}::parse(context, i)) { + has_custom_value = true; + ${prop} = Some(value); + continue + } + } + % endfor + + break + } + + if !has_custom_value { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } + + Ok(expanded! { + % for prop in sub_properties: + font_variant_${prop}: unwrap_or_initial!(font_variant_${prop}, ${prop}), + % endfor + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + #[allow(unused_assignments)] + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + + let has_none_ligatures = + % if engine == "gecko": + self.font_variant_ligatures == &FontVariantLigatures::NONE; + % else: + false; + % endif + + const TOTAL_SUBPROPS: usize = ${len(sub_properties)}; + let mut nb_normals = 0; + % for prop in sub_properties: + % if prop == "emoji": + if let Some(value) = self.font_variant_${prop} { + % else: + { + let value = self.font_variant_${prop}; + % endif + if value == &font_variant_${prop}::get_initial_specified_value() { + nb_normals += 1; + } + } + % if prop == "emoji": + else { + // The property was disabled, so we count it as 'normal' for the purpose + // of deciding how the shorthand can be serialized. + nb_normals += 1; + } + % endif + % endfor + + + if nb_normals > 0 && nb_normals == TOTAL_SUBPROPS { + dest.write_str("normal")?; + } else if has_none_ligatures { + if nb_normals == TOTAL_SUBPROPS - 1 { + // Serialize to 'none' if 'font-variant-ligatures' is set to 'none' and all other + // font feature properties are reset to their initial value. + dest.write_str("none")?; + } else { + return Ok(()) + } + } else { + let mut has_any = false; + % for prop in sub_properties: + % if prop == "emoji": + if let Some(value) = self.font_variant_${prop} { + % else: + { + let value = self.font_variant_${prop}; + % endif + if value != &font_variant_${prop}::get_initial_specified_value() { + if has_any { + dest.write_str(" ")?; + } + has_any = true; + value.to_css(dest)?; + } + } + % endfor + } + + Ok(()) + } + } +</%helpers:shorthand> diff --git a/servo/components/style/properties/shorthands/inherited_svg.mako.rs b/servo/components/style/properties/shorthands/inherited_svg.mako.rs new file mode 100644 index 0000000000..899fc6a464 --- /dev/null +++ b/servo/components/style/properties/shorthands/inherited_svg.mako.rs @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +<%helpers:shorthand + name="marker" + engines="gecko" + sub_properties="marker-start marker-end marker-mid" + spec="https://www.w3.org/TR/SVG2/painting.html#MarkerShorthand" +> + use crate::values::specified::url::UrlOrNone; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + use crate::parser::Parse; + let url = UrlOrNone::parse(context, input)?; + + Ok(expanded! { + marker_start: url.clone(), + marker_mid: url.clone(), + marker_end: url, + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + if self.marker_start == self.marker_mid && self.marker_mid == self.marker_end { + self.marker_start.to_css(dest) + } else { + Ok(()) + } + } + } +</%helpers:shorthand> diff --git a/servo/components/style/properties/shorthands/inherited_text.mako.rs b/servo/components/style/properties/shorthands/inherited_text.mako.rs new file mode 100644 index 0000000000..9eb278da05 --- /dev/null +++ b/servo/components/style/properties/shorthands/inherited_text.mako.rs @@ -0,0 +1,91 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +<%helpers:shorthand + name="text-emphasis" + engines="gecko" + sub_properties="text-emphasis-style text-emphasis-color" + derive_serialize="True" + spec="https://drafts.csswg.org/css-text-decor-3/#text-emphasis-property" +> + use crate::properties::longhands::{text_emphasis_color, text_emphasis_style}; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let mut color = None; + let mut style = None; + + loop { + if color.is_none() { + if let Ok(value) = input.try_parse(|input| text_emphasis_color::parse(context, input)) { + color = Some(value); + continue + } + } + if style.is_none() { + if let Ok(value) = input.try_parse(|input| text_emphasis_style::parse(context, input)) { + style = Some(value); + continue + } + } + break + } + if color.is_some() || style.is_some() { + Ok(expanded! { + text_emphasis_color: unwrap_or_initial!(text_emphasis_color, color), + text_emphasis_style: unwrap_or_initial!(text_emphasis_style, style), + }) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +</%helpers:shorthand> + +// CSS Compatibility +// https://compat.spec.whatwg.org/ +<%helpers:shorthand name="-webkit-text-stroke" + engines="gecko" + sub_properties="-webkit-text-stroke-width + -webkit-text-stroke-color" + derive_serialize="True" + spec="https://compat.spec.whatwg.org/#the-webkit-text-stroke"> + use crate::properties::longhands::{_webkit_text_stroke_color, _webkit_text_stroke_width}; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let mut color = None; + let mut width = None; + loop { + if color.is_none() { + if let Ok(value) = input.try_parse(|input| _webkit_text_stroke_color::parse(context, input)) { + color = Some(value); + continue + } + } + + if width.is_none() { + if let Ok(value) = input.try_parse(|input| _webkit_text_stroke_width::parse(context, input)) { + width = Some(value); + continue + } + } + break + } + + if color.is_some() || width.is_some() { + Ok(expanded! { + _webkit_text_stroke_color: unwrap_or_initial!(_webkit_text_stroke_color, color), + _webkit_text_stroke_width: unwrap_or_initial!(_webkit_text_stroke_width, width), + }) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +</%helpers:shorthand> diff --git a/servo/components/style/properties/shorthands/list.mako.rs b/servo/components/style/properties/shorthands/list.mako.rs new file mode 100644 index 0000000000..7f8c04026d --- /dev/null +++ b/servo/components/style/properties/shorthands/list.mako.rs @@ -0,0 +1,137 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +<%helpers:shorthand name="list-style" + engines="gecko servo-2013 servo-2020" + sub_properties="list-style-position list-style-image list-style-type" + spec="https://drafts.csswg.org/css-lists/#propdef-list-style"> + use crate::properties::longhands::{list_style_image, list_style_position, list_style_type}; + use crate::values::specified::Image; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + // `none` is ambiguous until we've finished parsing the shorthands, so we count the number + // of times we see it. + let mut nones = 0u8; + let (mut image, mut position, mut list_style_type, mut any) = (None, None, None, false); + loop { + if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() { + nones = nones + 1; + if nones > 2 { + return Err(input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent("none".into()))) + } + any = true; + continue + } + + if image.is_none() { + if let Ok(value) = input.try_parse(|input| list_style_image::parse(context, input)) { + image = Some(value); + any = true; + continue + } + } + + if position.is_none() { + if let Ok(value) = input.try_parse(|input| list_style_position::parse(context, input)) { + position = Some(value); + any = true; + continue + } + } + + // list-style-type must be checked the last, because it accepts + // arbitrary identifier for custom counter style, and thus may + // affect values of list-style-position. + if list_style_type.is_none() { + if let Ok(value) = input.try_parse(|input| list_style_type::parse(context, input)) { + list_style_type = Some(value); + any = true; + continue + } + } + break + } + + let position = unwrap_or_initial!(list_style_position, position); + + // If there are two `none`s, then we can't have a type or image; if there is one `none`, + // then we can't have both a type *and* an image; if there is no `none` then we're fine as + // long as we parsed something. + use self::list_style_type::SpecifiedValue as ListStyleType; + match (any, nones, list_style_type, image) { + (true, 2, None, None) => { + Ok(expanded! { + list_style_position: position, + list_style_image: Image::None, + list_style_type: ListStyleType::None, + }) + } + (true, 1, None, Some(image)) => { + Ok(expanded! { + list_style_position: position, + list_style_image: image, + list_style_type: ListStyleType::None, + }) + } + (true, 1, Some(list_style_type), None) => { + Ok(expanded! { + list_style_position: position, + list_style_image: Image::None, + list_style_type: list_style_type, + }) + } + (true, 1, None, None) => { + Ok(expanded! { + list_style_position: position, + list_style_image: Image::None, + list_style_type: ListStyleType::None, + }) + } + (true, 0, list_style_type, image) => { + Ok(expanded! { + list_style_position: position, + list_style_image: unwrap_or_initial!(list_style_image, image), + list_style_type: unwrap_or_initial!(list_style_type), + }) + } + _ => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + use longhands::list_style_position::SpecifiedValue as ListStylePosition; + use longhands::list_style_type::SpecifiedValue as ListStyleType; + use longhands::list_style_image::SpecifiedValue as ListStyleImage; + let mut have_one_non_initial_value = false; + if self.list_style_position != &ListStylePosition::Outside { + self.list_style_position.to_css(dest)?; + have_one_non_initial_value = true; + } + if self.list_style_image != &ListStyleImage::None { + if have_one_non_initial_value { + dest.write_str(" ")?; + } + self.list_style_image.to_css(dest)?; + have_one_non_initial_value = true; + } + if self.list_style_type != &ListStyleType::disc() { + if have_one_non_initial_value { + dest.write_str(" ")?; + } + self.list_style_type.to_css(dest)?; + have_one_non_initial_value = true; + } + if !have_one_non_initial_value { + self.list_style_position.to_css(dest)?; + } + Ok(()) + } + } +</%helpers:shorthand> diff --git a/servo/components/style/properties/shorthands/margin.mako.rs b/servo/components/style/properties/shorthands/margin.mako.rs new file mode 100644 index 0000000000..6b5bf7e467 --- /dev/null +++ b/servo/components/style/properties/shorthands/margin.mako.rs @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> +<% from data import DEFAULT_RULES_AND_PAGE %> + +${helpers.four_sides_shorthand( + "margin", + "margin-%s", + "specified::LengthPercentageOrAuto::parse", + engines="gecko servo-2013 servo-2020", + spec="https://drafts.csswg.org/css-box/#propdef-margin", + rule_types_allowed=DEFAULT_RULES_AND_PAGE, + allow_quirks="Yes", +)} + +${helpers.two_properties_shorthand( + "margin-block", + "margin-block-start", + "margin-block-end", + "specified::LengthPercentageOrAuto::parse", + engines="gecko servo-2013 servo-2020", + spec="https://drafts.csswg.org/css-logical/#propdef-margin-block" +)} + +${helpers.two_properties_shorthand( + "margin-inline", + "margin-inline-start", + "margin-inline-end", + "specified::LengthPercentageOrAuto::parse", + engines="gecko servo-2013 servo-2020", + spec="https://drafts.csswg.org/css-logical/#propdef-margin-inline" +)} + +${helpers.four_sides_shorthand( + "scroll-margin", + "scroll-margin-%s", + "specified::Length::parse", + engines="gecko", + spec="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-margin", +)} + +${helpers.two_properties_shorthand( + "scroll-margin-block", + "scroll-margin-block-start", + "scroll-margin-block-end", + "specified::Length::parse", + engines="gecko", + spec="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-margin-block", +)} + +${helpers.two_properties_shorthand( + "scroll-margin-inline", + "scroll-margin-inline-start", + "scroll-margin-inline-end", + "specified::Length::parse", + engines="gecko", + spec="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-margin-inline", +)} diff --git a/servo/components/style/properties/shorthands/outline.mako.rs b/servo/components/style/properties/shorthands/outline.mako.rs new file mode 100644 index 0000000000..e3b814d3d5 --- /dev/null +++ b/servo/components/style/properties/shorthands/outline.mako.rs @@ -0,0 +1,80 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +<%helpers:shorthand name="outline" + engines="gecko servo-2013" + sub_properties="outline-color outline-style outline-width" + spec="https://drafts.csswg.org/css-ui/#propdef-outline"> + use crate::properties::longhands::{outline_color, outline_width, outline_style}; + use crate::values::specified; + use crate::parser::Parse; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let _unused = context; + let mut color = None; + let mut style = None; + let mut width = None; + let mut any = false; + loop { + if color.is_none() { + if let Ok(value) = input.try_parse(|i| specified::Color::parse(context, i)) { + color = Some(value); + any = true; + continue + } + } + if style.is_none() { + if let Ok(value) = input.try_parse(|input| outline_style::parse(context, input)) { + style = Some(value); + any = true; + continue + } + } + if width.is_none() { + if let Ok(value) = input.try_parse(|input| outline_width::parse(context, input)) { + width = Some(value); + any = true; + continue + } + } + break + } + if any { + Ok(expanded! { + outline_color: unwrap_or_initial!(outline_color, color), + outline_style: unwrap_or_initial!(outline_style, style), + outline_width: unwrap_or_initial!(outline_width, width), + }) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + let mut wrote_value = false; + + % for name in "color style width".split(): + if *self.outline_${name} != outline_${name}::get_initial_specified_value() { + if wrote_value { + dest.write_str(" ")?; + } + self.outline_${name}.to_css(dest)?; + wrote_value = true; + } + % endfor + + if !wrote_value { + self.outline_style.to_css(dest)?; + } + + Ok(()) + } + } +</%helpers:shorthand> diff --git a/servo/components/style/properties/shorthands/padding.mako.rs b/servo/components/style/properties/shorthands/padding.mako.rs new file mode 100644 index 0000000000..11ddfed3b1 --- /dev/null +++ b/servo/components/style/properties/shorthands/padding.mako.rs @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +${helpers.four_sides_shorthand( + "padding", + "padding-%s", + "specified::NonNegativeLengthPercentage::parse", + engines="gecko servo-2013 servo-2020", + spec="https://drafts.csswg.org/css-box-3/#propdef-padding", + allow_quirks="Yes", +)} + +${helpers.two_properties_shorthand( + "padding-block", + "padding-block-start", + "padding-block-end", + "specified::NonNegativeLengthPercentage::parse", + engines="gecko servo-2013 servo-2020", + spec="https://drafts.csswg.org/css-logical/#propdef-padding-block" +)} + +${helpers.two_properties_shorthand( + "padding-inline", + "padding-inline-start", + "padding-inline-end", + "specified::NonNegativeLengthPercentage::parse", + engines="gecko servo-2013 servo-2020", + spec="https://drafts.csswg.org/css-logical/#propdef-padding-inline" +)} + +${helpers.four_sides_shorthand( + "scroll-padding", + "scroll-padding-%s", + "specified::NonNegativeLengthPercentageOrAuto::parse", + engines="gecko", + spec="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-padding" +)} + +${helpers.two_properties_shorthand( + "scroll-padding-block", + "scroll-padding-block-start", + "scroll-padding-block-end", + "specified::NonNegativeLengthPercentageOrAuto::parse", + engines="gecko", + spec="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-padding-block" +)} + +${helpers.two_properties_shorthand( + "scroll-padding-inline", + "scroll-padding-inline-start", + "scroll-padding-inline-end", + "specified::NonNegativeLengthPercentageOrAuto::parse", + engines="gecko", + spec="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-padding-inline" +)} diff --git a/servo/components/style/properties/shorthands/position.mako.rs b/servo/components/style/properties/shorthands/position.mako.rs new file mode 100644 index 0000000000..bc74254aff --- /dev/null +++ b/servo/components/style/properties/shorthands/position.mako.rs @@ -0,0 +1,879 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +<%helpers:shorthand name="flex-flow" + engines="gecko servo-2013 servo-2020", + servo_2020_pref="layout.flexbox.enabled", + sub_properties="flex-direction flex-wrap" + extra_prefixes="webkit" + derive_serialize="True" + spec="https://drafts.csswg.org/css-flexbox/#flex-flow-property"> + use crate::properties::longhands::{flex_direction, flex_wrap}; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let mut direction = None; + let mut wrap = None; + loop { + if direction.is_none() { + if let Ok(value) = input.try_parse(|input| flex_direction::parse(context, input)) { + direction = Some(value); + continue + } + } + if wrap.is_none() { + if let Ok(value) = input.try_parse(|input| flex_wrap::parse(context, input)) { + wrap = Some(value); + continue + } + } + break + } + + if direction.is_none() && wrap.is_none() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + Ok(expanded! { + flex_direction: unwrap_or_initial!(flex_direction, direction), + flex_wrap: unwrap_or_initial!(flex_wrap, wrap), + }) + } +</%helpers:shorthand> + +<%helpers:shorthand name="flex" + engines="gecko servo-2013 servo-2020", + servo_2020_pref="layout.flexbox.enabled", + sub_properties="flex-grow flex-shrink flex-basis" + extra_prefixes="webkit" + derive_serialize="True" + spec="https://drafts.csswg.org/css-flexbox/#flex-property"> + use crate::parser::Parse; + use crate::values::specified::NonNegativeNumber; + use crate::properties::longhands::flex_basis::SpecifiedValue as FlexBasis; + + fn parse_flexibility<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<(NonNegativeNumber, Option<NonNegativeNumber>),ParseError<'i>> { + let grow = NonNegativeNumber::parse(context, input)?; + let shrink = input.try_parse(|i| NonNegativeNumber::parse(context, i)).ok(); + Ok((grow, shrink)) + } + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let mut grow = None; + let mut shrink = None; + let mut basis = None; + + if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() { + return Ok(expanded! { + flex_grow: NonNegativeNumber::new(0.0), + flex_shrink: NonNegativeNumber::new(0.0), + flex_basis: FlexBasis::auto(), + }) + } + loop { + if grow.is_none() { + if let Ok((flex_grow, flex_shrink)) = input.try_parse(|i| parse_flexibility(context, i)) { + grow = Some(flex_grow); + shrink = flex_shrink; + continue + } + } + if basis.is_none() { + if let Ok(value) = input.try_parse(|input| FlexBasis::parse(context, input)) { + basis = Some(value); + continue + } + } + break + } + + if grow.is_none() && basis.is_none() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + Ok(expanded! { + flex_grow: grow.unwrap_or(NonNegativeNumber::new(1.0)), + flex_shrink: shrink.unwrap_or(NonNegativeNumber::new(1.0)), + // Per spec, this should be SpecifiedValue::zero(), but all + // browsers currently agree on using `0%`. This is a spec + // change which hasn't been adopted by browsers: + // https://github.com/w3c/csswg-drafts/commit/2c446befdf0f686217905bdd7c92409f6bd3921b + flex_basis: basis.unwrap_or(FlexBasis::zero_percent()), + }) + } +</%helpers:shorthand> + +<%helpers:shorthand + name="gap" + engines="gecko" + aliases="grid-gap" + sub_properties="row-gap column-gap" + spec="https://drafts.csswg.org/css-align-3/#gap-shorthand" +> + use crate::properties::longhands::{row_gap, column_gap}; + + pub fn parse_value<'i, 't>(context: &ParserContext, input: &mut Parser<'i, 't>) + -> Result<Longhands, ParseError<'i>> { + let r_gap = row_gap::parse(context, input)?; + let c_gap = input.try_parse(|input| column_gap::parse(context, input)).unwrap_or(r_gap.clone()); + + Ok(expanded! { + row_gap: r_gap, + column_gap: c_gap, + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + if self.row_gap == self.column_gap { + self.row_gap.to_css(dest) + } else { + self.row_gap.to_css(dest)?; + dest.write_str(" ")?; + self.column_gap.to_css(dest) + } + } + } + +</%helpers:shorthand> + +% for kind in ["row", "column"]: +<%helpers:shorthand + name="grid-${kind}" + sub_properties="grid-${kind}-start grid-${kind}-end" + engines="gecko", + spec="https://drafts.csswg.org/css-grid/#propdef-grid-${kind}" +> + use crate::values::specified::GridLine; + use crate::parser::Parse; + use crate::Zero; + + // NOTE: Since both the shorthands have the same code, we should (re-)use code from one to implement + // the other. This might not be a big deal for now, but we should consider looking into this in the future + // to limit the amount of code generated. + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let start = input.try_parse(|i| GridLine::parse(context, i))?; + let end = if input.try_parse(|i| i.expect_delim('/')).is_ok() { + GridLine::parse(context, input)? + } else { + let mut line = GridLine::auto(); + if start.line_num.is_zero() && !start.is_span { + line.ident = start.ident.clone(); // ident from start value should be taken + } + + line + }; + + Ok(expanded! { + grid_${kind}_start: start, + grid_${kind}_end: end, + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + // Return the shortest possible serialization of the `grid-${kind}-[start/end]` values. + // This function exploits the opportunities to omit the end value per this spec text: + // + // https://drafts.csswg.org/css-grid/#propdef-grid-column + // "When the second value is omitted, if the first value is a <custom-ident>, + // the grid-row-end/grid-column-end longhand is also set to that <custom-ident>; + // otherwise, it is set to auto." + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + self.grid_${kind}_start.to_css(dest)?; + if self.grid_${kind}_start.can_omit(self.grid_${kind}_end) { + return Ok(()); // the end value is redundant + } + dest.write_str(" / ")?; + self.grid_${kind}_end.to_css(dest) + } + } +</%helpers:shorthand> +% endfor + +<%helpers:shorthand + name="grid-area" + engines="gecko" + sub_properties="grid-row-start grid-row-end grid-column-start grid-column-end" + spec="https://drafts.csswg.org/css-grid/#propdef-grid-area" +> + use crate::values::specified::GridLine; + use crate::parser::Parse; + use crate::Zero; + + // The code is the same as `grid-{row,column}` except that this can have four values at most. + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + fn line_with_ident_from(other: &GridLine) -> GridLine { + let mut this = GridLine::auto(); + if other.line_num.is_zero() && !other.is_span { + this.ident = other.ident.clone(); + } + + this + } + + let row_start = input.try_parse(|i| GridLine::parse(context, i))?; + let (column_start, row_end, column_end) = if input.try_parse(|i| i.expect_delim('/')).is_ok() { + let column_start = GridLine::parse(context, input)?; + let (row_end, column_end) = if input.try_parse(|i| i.expect_delim('/')).is_ok() { + let row_end = GridLine::parse(context, input)?; + let column_end = if input.try_parse(|i| i.expect_delim('/')).is_ok() { + GridLine::parse(context, input)? + } else { // grid-column-end has not been given + line_with_ident_from(&column_start) + }; + + (row_end, column_end) + } else { // grid-row-start and grid-column-start has been given + let row_end = line_with_ident_from(&row_start); + let column_end = line_with_ident_from(&column_start); + (row_end, column_end) + }; + + (column_start, row_end, column_end) + } else { // only grid-row-start is given + let line = line_with_ident_from(&row_start); + (line.clone(), line.clone(), line) + }; + + Ok(expanded! { + grid_row_start: row_start, + grid_row_end: row_end, + grid_column_start: column_start, + grid_column_end: column_end, + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + // Return the shortest possible serialization of the `grid-[column/row]-[start/end]` values. + // This function exploits the opportunities to omit trailing values per this spec text: + // + // https://drafts.csswg.org/css-grid/#propdef-grid-area + // "If four <grid-line> values are specified, grid-row-start is set to the first value, + // grid-column-start is set to the second value, grid-row-end is set to the third value, + // and grid-column-end is set to the fourth value. + // + // When grid-column-end is omitted, if grid-column-start is a <custom-ident>, + // grid-column-end is set to that <custom-ident>; otherwise, it is set to auto. + // + // When grid-row-end is omitted, if grid-row-start is a <custom-ident>, grid-row-end is + // set to that <custom-ident>; otherwise, it is set to auto. + // + // When grid-column-start is omitted, if grid-row-start is a <custom-ident>, all four + // longhands are set to that value. Otherwise, it is set to auto." + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + self.grid_row_start.to_css(dest)?; + let mut trailing_values = 3; + if self.grid_column_start.can_omit(self.grid_column_end) { + trailing_values -= 1; + if self.grid_row_start.can_omit(self.grid_row_end) { + trailing_values -= 1; + if self.grid_row_start.can_omit(self.grid_column_start) { + trailing_values -= 1; + } + } + } + let values = [&self.grid_column_start, &self.grid_row_end, &self.grid_column_end]; + for value in values.iter().take(trailing_values) { + dest.write_str(" / ")?; + value.to_css(dest)?; + } + Ok(()) + } + } +</%helpers:shorthand> + +<%helpers:shorthand + name="grid-template" + engines="gecko" + sub_properties="grid-template-rows grid-template-columns grid-template-areas" + spec="https://drafts.csswg.org/css-grid/#propdef-grid-template" +> + use crate::parser::Parse; + use servo_arc::Arc; + use crate::values::generics::grid::{TrackSize, TrackList}; + use crate::values::generics::grid::{TrackListValue, concat_serialize_idents}; + use crate::values::specified::{GridTemplateComponent, GenericGridTemplateComponent}; + use crate::values::specified::grid::parse_line_names; + use crate::values::specified::position::{GridTemplateAreas, TemplateAreas, TemplateAreasArc}; + + /// Parsing for `<grid-template>` shorthand (also used by `grid` shorthand). + pub fn parse_grid_template<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<(GridTemplateComponent, GridTemplateComponent, GridTemplateAreas), ParseError<'i>> { + // Other shorthand sub properties also parse the `none` keyword and this shorthand + // should know after this keyword there is nothing to parse. Otherwise it gets + // confused and rejects the sub properties that contains `none`. + <% keywords = { + "none": "GenericGridTemplateComponent::None", + } + %> + % for keyword, rust_type in keywords.items(): + if let Ok(x) = input.try_parse(|i| { + if i.try_parse(|i| i.expect_ident_matching("${keyword}")).is_ok() { + if !i.is_exhausted() { + return Err(()); + } + return Ok((${rust_type}, ${rust_type}, GridTemplateAreas::None)); + } + Err(()) + }) { + return Ok(x); + } + % endfor + + let first_line_names = input.try_parse(parse_line_names).unwrap_or_default(); + if let Ok(string) = input.try_parse(|i| i.expect_string().map(|s| s.as_ref().to_owned().into())) { + let mut strings = vec![]; + let mut values = vec![]; + let mut line_names = vec![]; + line_names.push(first_line_names); + strings.push(string); + loop { + let size = input.try_parse(|i| TrackSize::parse(context, i)).unwrap_or_default(); + values.push(TrackListValue::TrackSize(size)); + let mut names = input.try_parse(parse_line_names).unwrap_or_default(); + let more_names = input.try_parse(parse_line_names); + + match input.try_parse(|i| i.expect_string().map(|s| s.as_ref().to_owned().into())) { + Ok(string) => { + strings.push(string); + if let Ok(v) = more_names { + // We got `[names] [more_names] "string"` - merge the two name lists. + let mut names_vec = names.into_vec(); + names_vec.extend(v.into_iter()); + names = names_vec.into(); + } + line_names.push(names); + }, + Err(e) => { + if more_names.is_ok() { + // We've parsed `"string" [names] [more_names]` but then failed to parse another `"string"`. + // The grammar doesn't allow two trailing `<line-names>` so this is an invalid value. + return Err(e.into()); + } + // only the named area determines whether we should bail out + line_names.push(names); + break + }, + }; + } + + if line_names.len() == values.len() { + // should be one longer than track sizes + line_names.push(Default::default()); + } + + let template_areas = TemplateAreas::from_vec(strings) + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))?; + let template_rows = TrackList { + values: values.into(), + line_names: line_names.into(), + auto_repeat_index: std::usize::MAX, + }; + + let template_cols = if input.try_parse(|i| i.expect_delim('/')).is_ok() { + let value = GridTemplateComponent::parse_without_none(context, input)?; + if let GenericGridTemplateComponent::TrackList(ref list) = value { + if !list.is_explicit() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } + + value + } else { + GridTemplateComponent::default() + }; + + Ok(( + GenericGridTemplateComponent::TrackList(Box::new(template_rows)), + template_cols, + GridTemplateAreas::Areas(TemplateAreasArc(Arc::new(template_areas))) + )) + } else { + let mut template_rows = GridTemplateComponent::parse(context, input)?; + if let GenericGridTemplateComponent::TrackList(ref mut list) = template_rows { + // Fist line names are parsed already and it shouldn't be parsed again. + // If line names are not empty, that means given property value is not acceptable + if list.line_names[0].is_empty() { + list.line_names[0] = first_line_names; // won't panic + } else { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + } + + input.expect_delim('/')?; + Ok((template_rows, GridTemplateComponent::parse(context, input)?, GridTemplateAreas::None)) + } + } + + #[inline] + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let (rows, columns, areas) = parse_grid_template(context, input)?; + Ok(expanded! { + grid_template_rows: rows, + grid_template_columns: columns, + grid_template_areas: areas, + }) + } + + /// Serialization for `<grid-template>` shorthand (also used by `grid` shorthand). + pub fn serialize_grid_template<W>( + template_rows: &GridTemplateComponent, + template_columns: &GridTemplateComponent, + template_areas: &GridTemplateAreas, + dest: &mut CssWriter<W>, + ) -> fmt::Result + where + W: Write { + match *template_areas { + GridTemplateAreas::None => { + if template_rows.is_initial() && template_columns.is_initial() { + return GridTemplateComponent::default().to_css(dest); + } + template_rows.to_css(dest)?; + dest.write_str(" / ")?; + template_columns.to_css(dest) + }, + GridTemplateAreas::Areas(ref areas) => { + // The length of template-area and template-rows values should be equal. + if areas.0.strings.len() != template_rows.track_list_len() { + return Ok(()); + } + + let track_list = match *template_rows { + GenericGridTemplateComponent::TrackList(ref list) => { + // We should fail if there is a `repeat` function. + // `grid` and `grid-template` shorthands doesn't accept + // that. Only longhand accepts. + if !list.is_explicit() { + return Ok(()); + } + list + }, + // Others template components shouldn't exist with normal shorthand values. + // But if we need to serialize a group of longhand sub-properties for + // the shorthand, we should be able to return empty string instead of crashing. + _ => return Ok(()), + }; + + // We need to check some values that longhand accepts but shorthands don't. + match *template_columns { + // We should fail if there is a `repeat` function. `grid` and + // `grid-template` shorthands doesn't accept that. Only longhand accepts that. + GenericGridTemplateComponent::TrackList(ref list) => { + if !list.is_explicit() { + return Ok(()); + } + }, + // Also the shorthands don't accept subgrids unlike longhand. + // We should fail without an error here. + GenericGridTemplateComponent::Subgrid(_) => { + return Ok(()); + }, + _ => {}, + } + + let mut names_iter = track_list.line_names.iter(); + for (((i, string), names), value) in areas.0.strings.iter().enumerate() + .zip(&mut names_iter) + .zip(track_list.values.iter()) { + if i > 0 { + dest.write_str(" ")?; + } + + if !names.is_empty() { + concat_serialize_idents("[", "] ", names, " ", dest)?; + } + + string.to_css(dest)?; + + // If the track size is the initial value then it's redundant here. + if !value.is_initial() { + dest.write_str(" ")?; + value.to_css(dest)?; + } + } + + if let Some(names) = names_iter.next() { + concat_serialize_idents(" [", "]", names, " ", dest)?; + } + + if let GenericGridTemplateComponent::TrackList(ref list) = *template_columns { + dest.write_str(" / ")?; + list.to_css(dest)?; + } + + Ok(()) + }, + } + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + #[inline] + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + serialize_grid_template( + self.grid_template_rows, + self.grid_template_columns, + self.grid_template_areas, + dest + ) + } + } +</%helpers:shorthand> + +<%helpers:shorthand + name="grid" + engines="gecko" + sub_properties="grid-template-rows grid-template-columns grid-template-areas + grid-auto-rows grid-auto-columns grid-auto-flow" + spec="https://drafts.csswg.org/css-grid/#propdef-grid" +> + use crate::parser::Parse; + use crate::properties::longhands::{grid_auto_columns, grid_auto_rows, grid_auto_flow}; + use crate::values::generics::grid::GridTemplateComponent; + use crate::values::specified::{GenericGridTemplateComponent, ImplicitGridTracks}; + use crate::values::specified::position::{GridAutoFlow, GridTemplateAreas}; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let mut temp_rows = GridTemplateComponent::default(); + let mut temp_cols = GridTemplateComponent::default(); + let mut temp_areas = GridTemplateAreas::None; + let mut auto_rows = ImplicitGridTracks::default(); + let mut auto_cols = ImplicitGridTracks::default(); + let mut flow = grid_auto_flow::get_initial_value(); + + fn parse_auto_flow<'i, 't>( + input: &mut Parser<'i, 't>, + is_row: bool, + ) -> Result<GridAutoFlow, ParseError<'i>> { + let mut track = None; + let mut dense = GridAutoFlow::empty(); + + for _ in 0..2 { + if input.try_parse(|i| i.expect_ident_matching("auto-flow")).is_ok() { + track = if is_row { + Some(GridAutoFlow::ROW) + } else { + Some(GridAutoFlow::COLUMN) + }; + } else if input.try_parse(|i| i.expect_ident_matching("dense")).is_ok() { + dense = GridAutoFlow::DENSE + } else { + break + } + } + + if track.is_some() { + Ok(track.unwrap() | dense) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } + + if let Ok((rows, cols, areas)) = input.try_parse(|i| super::grid_template::parse_grid_template(context, i)) { + temp_rows = rows; + temp_cols = cols; + temp_areas = areas; + } else if let Ok(rows) = input.try_parse(|i| GridTemplateComponent::parse(context, i)) { + temp_rows = rows; + input.expect_delim('/')?; + flow = parse_auto_flow(input, false)?; + auto_cols = input.try_parse(|i| grid_auto_columns::parse(context, i)).unwrap_or_default(); + } else { + flow = parse_auto_flow(input, true)?; + auto_rows = input.try_parse(|i| grid_auto_rows::parse(context, i)).unwrap_or_default(); + input.expect_delim('/')?; + temp_cols = GridTemplateComponent::parse(context, input)?; + } + + Ok(expanded! { + grid_template_rows: temp_rows, + grid_template_columns: temp_cols, + grid_template_areas: temp_areas, + grid_auto_rows: auto_rows, + grid_auto_columns: auto_cols, + grid_auto_flow: flow, + }) + } + + impl<'a> LonghandsToSerialize<'a> { + /// Returns true if other sub properties except template-{rows,columns} are initial. + fn is_grid_template(&self) -> bool { + self.grid_auto_rows.is_initial() && + self.grid_auto_columns.is_initial() && + *self.grid_auto_flow == grid_auto_flow::get_initial_value() + } + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + if self.is_grid_template() { + return super::grid_template::serialize_grid_template( + self.grid_template_rows, + self.grid_template_columns, + self.grid_template_areas, + dest + ); + } + + if *self.grid_template_areas != GridTemplateAreas::None { + // No other syntax can set the template areas, so fail to + // serialize. + return Ok(()); + } + + if self.grid_auto_flow.contains(GridAutoFlow::COLUMN) { + // It should fail to serialize if other branch of the if condition's values are set. + if !self.grid_auto_rows.is_initial() || + !self.grid_template_columns.is_initial() { + return Ok(()); + } + + // It should fail to serialize if template-rows value is not Explicit. + if let GenericGridTemplateComponent::TrackList(ref list) = *self.grid_template_rows { + if !list.is_explicit() { + return Ok(()); + } + } + + self.grid_template_rows.to_css(dest)?; + dest.write_str(" / auto-flow")?; + if self.grid_auto_flow.contains(GridAutoFlow::DENSE) { + dest.write_str(" dense")?; + } + + if !self.grid_auto_columns.is_initial() { + dest.write_str(" ")?; + self.grid_auto_columns.to_css(dest)?; + } + + return Ok(()); + } + + // It should fail to serialize if other branch of the if condition's values are set. + if !self.grid_auto_columns.is_initial() || + !self.grid_template_rows.is_initial() { + return Ok(()); + } + + // It should fail to serialize if template-column value is not Explicit. + if let GenericGridTemplateComponent::TrackList(ref list) = *self.grid_template_columns { + if !list.is_explicit() { + return Ok(()); + } + } + + dest.write_str("auto-flow")?; + if self.grid_auto_flow.contains(GridAutoFlow::DENSE) { + dest.write_str(" dense")?; + } + + if !self.grid_auto_rows.is_initial() { + dest.write_str(" ")?; + self.grid_auto_rows.to_css(dest)?; + } + + dest.write_str(" / ")?; + self.grid_template_columns.to_css(dest)?; + Ok(()) + } + } +</%helpers:shorthand> + +<%helpers:shorthand + name="place-content" + engines="gecko" + sub_properties="align-content justify-content" + spec="https://drafts.csswg.org/css-align/#propdef-place-content" +> + use crate::values::specified::align::{AlignContent, JustifyContent, ContentDistribution, AxisDirection}; + + pub fn parse_value<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let align_content = + ContentDistribution::parse(input, AxisDirection::Block)?; + + let justify_content = input.try_parse(|input| { + ContentDistribution::parse(input, AxisDirection::Inline) + }); + + let justify_content = match justify_content { + Ok(v) => v, + Err(..) => { + // https://drafts.csswg.org/css-align-3/#place-content: + // + // The second value is assigned to justify-content; if + // omitted, it is copied from the first value, unless that + // value is a <baseline-position> in which case it is + // defaulted to start. + // + if !align_content.is_baseline_position() { + align_content + } else { + ContentDistribution::start() + } + } + }; + + Ok(expanded! { + align_content: AlignContent(align_content), + justify_content: JustifyContent(justify_content), + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + self.align_content.to_css(dest)?; + if self.align_content.0 != self.justify_content.0 { + dest.write_str(" ")?; + self.justify_content.to_css(dest)?; + } + Ok(()) + } + } +</%helpers:shorthand> + +<%helpers:shorthand + name="place-self" + engines="gecko" + sub_properties="align-self justify-self" + spec="https://drafts.csswg.org/css-align/#place-self-property" +> + use crate::values::specified::align::{AlignSelf, JustifySelf, SelfAlignment, AxisDirection}; + + pub fn parse_value<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let align = SelfAlignment::parse(input, AxisDirection::Block)?; + let justify = input.try_parse(|input| SelfAlignment::parse(input, AxisDirection::Inline)); + + let justify = match justify { + Ok(v) => v, + Err(..) => { + debug_assert!(align.is_valid_on_both_axes()); + align + } + }; + + Ok(expanded! { + align_self: AlignSelf(align), + justify_self: JustifySelf(justify), + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + self.align_self.to_css(dest)?; + if self.align_self.0 != self.justify_self.0 { + dest.write_str(" ")?; + self.justify_self.to_css(dest)?; + } + Ok(()) + } + } +</%helpers:shorthand> + +<%helpers:shorthand + name="place-items" + engines="gecko" + sub_properties="align-items justify-items" + spec="https://drafts.csswg.org/css-align/#place-items-property" +> + use crate::values::specified::align::{AlignItems, JustifyItems}; + use crate::parser::Parse; + + impl From<AlignItems> for JustifyItems { + fn from(align: AlignItems) -> JustifyItems { + JustifyItems(align.0) + } + } + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let align = AlignItems::parse(context, input)?; + let justify = + input.try_parse(|input| JustifyItems::parse(context, input)) + .unwrap_or_else(|_| JustifyItems::from(align)); + + Ok(expanded! { + align_items: align, + justify_items: justify, + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + self.align_items.to_css(dest)?; + if self.align_items.0 != self.justify_items.0 { + dest.write_str(" ")?; + self.justify_items.to_css(dest)?; + } + + Ok(()) + } + } +</%helpers:shorthand> + +// See https://github.com/w3c/csswg-drafts/issues/3525 for the quirks stuff. +${helpers.four_sides_shorthand( + "inset", + "%s", + "specified::LengthPercentageOrAuto::parse", + engines="gecko servo-2013", + spec="https://drafts.csswg.org/css-logical/#propdef-inset", + allow_quirks="No", +)} + +${helpers.two_properties_shorthand( + "inset-block", + "inset-block-start", + "inset-block-end", + "specified::LengthPercentageOrAuto::parse", + engines="gecko servo-2013", + spec="https://drafts.csswg.org/css-logical/#propdef-inset-block" +)} + +${helpers.two_properties_shorthand( + "inset-inline", + "inset-inline-start", + "inset-inline-end", + "specified::LengthPercentageOrAuto::parse", + engines="gecko servo-2013", + spec="https://drafts.csswg.org/css-logical/#propdef-inset-inline" +)} + +${helpers.two_properties_shorthand( + "contain-intrinsic-size", + "contain-intrinsic-width", + "contain-intrinsic-height", + engines="gecko", + gecko_pref="layout.css.contain-intrinsic-size.enabled", + spec="https://drafts.csswg.org/css-sizing-4/#intrinsic-size-override", +)} diff --git a/servo/components/style/properties/shorthands/svg.mako.rs b/servo/components/style/properties/shorthands/svg.mako.rs new file mode 100644 index 0000000000..99461a5148 --- /dev/null +++ b/servo/components/style/properties/shorthands/svg.mako.rs @@ -0,0 +1,258 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +<%helpers:shorthand name="mask" engines="gecko" extra_prefixes="webkit" + flags="SHORTHAND_IN_GETCS" + sub_properties="mask-mode mask-repeat mask-clip mask-origin mask-composite mask-position-x + mask-position-y mask-size mask-image" + spec="https://drafts.fxtf.org/css-masking/#propdef-mask"> + use crate::properties::longhands::{mask_mode, mask_repeat, mask_clip, mask_origin, mask_composite, mask_position_x, + mask_position_y}; + use crate::properties::longhands::{mask_size, mask_image}; + use crate::values::specified::{Position, PositionComponent}; + use crate::parser::Parse; + + // FIXME(emilio): These two mask types should be the same! + impl From<mask_origin::single_value::SpecifiedValue> for mask_clip::single_value::SpecifiedValue { + fn from(origin: mask_origin::single_value::SpecifiedValue) -> mask_clip::single_value::SpecifiedValue { + match origin { + mask_origin::single_value::SpecifiedValue::ContentBox => + mask_clip::single_value::SpecifiedValue::ContentBox, + mask_origin::single_value::SpecifiedValue::PaddingBox => + mask_clip::single_value::SpecifiedValue::PaddingBox , + mask_origin::single_value::SpecifiedValue::BorderBox => + mask_clip::single_value::SpecifiedValue::BorderBox, + % if engine == "gecko": + mask_origin::single_value::SpecifiedValue::FillBox => + mask_clip::single_value::SpecifiedValue::FillBox , + mask_origin::single_value::SpecifiedValue::StrokeBox => + mask_clip::single_value::SpecifiedValue::StrokeBox, + mask_origin::single_value::SpecifiedValue::ViewBox=> + mask_clip::single_value::SpecifiedValue::ViewBox, + % endif + } + } + } + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + % for name in "image mode position_x position_y size repeat origin clip composite".split(): + // Vec grows from 0 to 4 by default on first push(). So allocate with + // capacity 1, so in the common case of only one item we don't way + // overallocate, then shrink. Note that we always push at least one + // item if parsing succeeds. + let mut mask_${name} = Vec::with_capacity(1); + % endfor + + input.parse_comma_separated(|input| { + % for name in "image mode position size repeat origin clip composite".split(): + let mut ${name} = None; + % endfor + loop { + if image.is_none() { + if let Ok(value) = input.try_parse(|input| mask_image::single_value + ::parse(context, input)) { + image = Some(value); + continue + } + } + if position.is_none() { + if let Ok(value) = input.try_parse(|input| Position::parse(context, input)) { + position = Some(value); + + // Parse mask size, if applicable. + size = input.try_parse(|input| { + input.expect_delim('/')?; + mask_size::single_value::parse(context, input) + }).ok(); + + continue + } + } + % for name in "repeat origin clip composite mode".split(): + if ${name}.is_none() { + if let Ok(value) = input.try_parse(|input| mask_${name}::single_value + ::parse(context, input)) { + ${name} = Some(value); + continue + } + } + % endfor + break + } + if clip.is_none() { + if let Some(origin) = origin { + clip = Some(mask_clip::single_value::SpecifiedValue::from(origin)); + } + } + let mut any = false; + % for name in "image mode position size repeat origin clip composite".split(): + any = any || ${name}.is_some(); + % endfor + if any { + if let Some(position) = position { + mask_position_x.push(position.horizontal); + mask_position_y.push(position.vertical); + } else { + mask_position_x.push(PositionComponent::zero()); + mask_position_y.push(PositionComponent::zero()); + } + % for name in "image mode size repeat origin clip composite".split(): + if let Some(m_${name}) = ${name} { + mask_${name}.push(m_${name}); + } else { + mask_${name}.push(mask_${name}::single_value + ::get_initial_specified_value()); + } + % endfor + Ok(()) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + })?; + + Ok(expanded! { + % for name in "image mode position_x position_y size repeat origin clip composite".split(): + mask_${name}: mask_${name}::SpecifiedValue(mask_${name}.into()), + % endfor + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + use crate::properties::longhands::mask_origin::single_value::computed_value::T as Origin; + use crate::properties::longhands::mask_clip::single_value::computed_value::T as Clip; + + let len = self.mask_image.0.len(); + if len == 0 { + return Ok(()); + } + % for name in "mode position_x position_y size repeat origin clip composite".split(): + if self.mask_${name}.0.len() != len { + return Ok(()); + } + % endfor + + for i in 0..len { + if i > 0 { + dest.write_str(", ")?; + } + + % for name in "image mode position_x position_y size repeat origin clip composite".split(): + let ${name} = &self.mask_${name}.0[i]; + % endfor + + image.to_css(dest)?; + + if *mode != mask_mode::single_value::get_initial_specified_value() { + dest.write_str(" ")?; + mode.to_css(dest)?; + } + + if *position_x != PositionComponent::zero() || + *position_y != PositionComponent::zero() || + *size != mask_size::single_value::get_initial_specified_value() + { + dest.write_str(" ")?; + Position { + horizontal: position_x.clone(), + vertical: position_y.clone() + }.to_css(dest)?; + + if *size != mask_size::single_value::get_initial_specified_value() { + dest.write_str(" / ")?; + size.to_css(dest)?; + } + } + + if *repeat != mask_repeat::single_value::get_initial_specified_value() { + dest.write_str(" ")?; + repeat.to_css(dest)?; + } + + if *origin != Origin::BorderBox || *clip != Clip::BorderBox { + dest.write_str(" ")?; + origin.to_css(dest)?; + if *clip != From::from(*origin) { + dest.write_str(" ")?; + clip.to_css(dest)?; + } + } + + if *composite != mask_composite::single_value::get_initial_specified_value() { + dest.write_str(" ")?; + composite.to_css(dest)?; + } + } + + Ok(()) + } + } +</%helpers:shorthand> + +<%helpers:shorthand name="mask-position" engines="gecko" extra_prefixes="webkit" + flags="SHORTHAND_IN_GETCS" + sub_properties="mask-position-x mask-position-y" + spec="https://drafts.csswg.org/css-masks-4/#the-mask-position"> + use crate::properties::longhands::{mask_position_x,mask_position_y}; + use crate::values::specified::position::Position; + use crate::parser::Parse; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + // Vec grows from 0 to 4 by default on first push(). So allocate with + // capacity 1, so in the common case of only one item we don't way + // overallocate, then shrink. Note that we always push at least one + // item if parsing succeeds. + let mut position_x = Vec::with_capacity(1); + let mut position_y = Vec::with_capacity(1); + let mut any = false; + + input.parse_comma_separated(|input| { + let value = Position::parse(context, input)?; + position_x.push(value.horizontal); + position_y.push(value.vertical); + any = true; + Ok(()) + })?; + + if !any { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + + Ok(expanded! { + mask_position_x: mask_position_x::SpecifiedValue(position_x.into()), + mask_position_y: mask_position_y::SpecifiedValue(position_y.into()), + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + let len = self.mask_position_x.0.len(); + if len == 0 || self.mask_position_y.0.len() != len { + return Ok(()); + } + + for i in 0..len { + Position { + horizontal: self.mask_position_x.0[i].clone(), + vertical: self.mask_position_y.0[i].clone() + }.to_css(dest)?; + + if i < len - 1 { + dest.write_str(", ")?; + } + } + + Ok(()) + } + } +</%helpers:shorthand> diff --git a/servo/components/style/properties/shorthands/text.mako.rs b/servo/components/style/properties/shorthands/text.mako.rs new file mode 100644 index 0000000000..9145cc4343 --- /dev/null +++ b/servo/components/style/properties/shorthands/text.mako.rs @@ -0,0 +1,120 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +<%helpers:shorthand name="text-decoration" + engines="gecko servo-2013 servo-2020" + flags="SHORTHAND_IN_GETCS" + sub_properties="text-decoration-line + ${' text-decoration-style text-decoration-color text-decoration-thickness' if engine == 'gecko' else ''}" + spec="https://drafts.csswg.org/css-text-decor/#propdef-text-decoration"> + % if engine == "gecko": + use crate::values::specified; + use crate::properties::longhands::{text_decoration_style, text_decoration_color, text_decoration_thickness}; + % endif + use crate::properties::longhands::text_decoration_line; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + % if engine == "gecko": + let (mut line, mut style, mut color, mut thickness, mut any) = (None, None, None, None, false); + % else: + let (mut line, mut any) = (None, false); + % endif + + loop { + macro_rules! parse_component { + ($value:ident, $module:ident) => ( + if $value.is_none() { + if let Ok(value) = input.try_parse(|input| $module::parse(context, input)) { + $value = Some(value); + any = true; + continue; + } + } + ) + } + + parse_component!(line, text_decoration_line); + + % if engine == "gecko": + parse_component!(style, text_decoration_style); + parse_component!(color, text_decoration_color); + parse_component!(thickness, text_decoration_thickness); + % endif + + break; + } + + if !any { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(expanded! { + text_decoration_line: unwrap_or_initial!(text_decoration_line, line), + + % if engine == "gecko": + text_decoration_style: unwrap_or_initial!(text_decoration_style, style), + text_decoration_color: unwrap_or_initial!(text_decoration_color, color), + text_decoration_thickness: unwrap_or_initial!(text_decoration_thickness, thickness), + % endif + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + #[allow(unused)] + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + use crate::values::specified::TextDecorationLine; + + let (is_solid_style, is_current_color, is_auto_thickness) = + ( + % if engine == "gecko": + *self.text_decoration_style == text_decoration_style::SpecifiedValue::Solid, + *self.text_decoration_color == specified::Color::CurrentColor, + self.text_decoration_thickness.is_auto() + % else: + true, true, true + % endif + ); + + let mut has_value = false; + let is_none = *self.text_decoration_line == TextDecorationLine::none(); + if (is_solid_style && is_current_color && is_auto_thickness) || !is_none { + self.text_decoration_line.to_css(dest)?; + has_value = true; + } + + if !is_auto_thickness { + if has_value { + dest.write_str(" ")?; + } + self.text_decoration_thickness.to_css(dest)?; + has_value = true; + } + + % if engine == "gecko": + if !is_solid_style { + if has_value { + dest.write_str(" ")?; + } + self.text_decoration_style.to_css(dest)?; + has_value = true; + } + + if !is_current_color { + if has_value { + dest.write_str(" ")?; + } + self.text_decoration_color.to_css(dest)?; + has_value = true; + } + % endif + + Ok(()) + } + } +</%helpers:shorthand> diff --git a/servo/components/style/properties/shorthands/ui.mako.rs b/servo/components/style/properties/shorthands/ui.mako.rs new file mode 100644 index 0000000000..fc84e74e23 --- /dev/null +++ b/servo/components/style/properties/shorthands/ui.mako.rs @@ -0,0 +1,382 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +macro_rules! try_parse_one { + ($context: expr, $input: expr, $var: ident, $prop_module: ident) => { + if $var.is_none() { + if let Ok(value) = $input.try_parse(|i| { + $prop_module::single_value::parse($context, i) + }) { + $var = Some(value); + continue; + } + } + }; +} + +<%helpers:shorthand name="transition" + engines="gecko servo-2013 servo-2020" + extra_prefixes="moz:layout.css.prefixes.transitions webkit" + sub_properties="transition-property transition-duration + transition-timing-function + transition-delay" + spec="https://drafts.csswg.org/css-transitions/#propdef-transition"> + use crate::parser::Parse; + % for prop in "delay duration property timing_function".split(): + use crate::properties::longhands::transition_${prop}; + % endfor + use crate::values::specified::TransitionProperty; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + struct SingleTransition { + % for prop in "duration timing_function delay".split(): + transition_${prop}: transition_${prop}::SingleSpecifiedValue, + % endfor + // Unlike other properties, transition-property uses an Option<> to + // represent 'none' as `None`. + transition_property: Option<TransitionProperty>, + } + + fn parse_one_transition<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<SingleTransition,ParseError<'i>> { + % for prop in "property duration timing_function delay".split(): + let mut ${prop} = None; + % endfor + + let mut parsed = 0; + loop { + parsed += 1; + + try_parse_one!(context, input, duration, transition_duration); + try_parse_one!(context, input, timing_function, transition_timing_function); + try_parse_one!(context, input, delay, transition_delay); + // Must check 'transition-property' after 'transition-timing-function' since + // 'transition-property' accepts any keyword. + if property.is_none() { + if let Ok(value) = input.try_parse(|i| TransitionProperty::parse(context, i)) { + property = Some(Some(value)); + continue; + } + + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + // 'none' is not a valid value for <single-transition-property>, + // so it's not acceptable in the function above. + property = Some(None); + continue; + } + } + + parsed -= 1; + break + } + + if parsed != 0 { + Ok(SingleTransition { + % for prop in "duration timing_function delay".split(): + transition_${prop}: ${prop}.unwrap_or_else(transition_${prop}::single_value + ::get_initial_specified_value), + % endfor + transition_property: property.unwrap_or( + Some(transition_property::single_value::get_initial_specified_value())), + }) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } + + % for prop in "property duration timing_function delay".split(): + let mut ${prop}s = Vec::new(); + % endfor + + let results = input.parse_comma_separated(|i| parse_one_transition(context, i))?; + let multiple_items = results.len() >= 2; + for result in results { + if let Some(value) = result.transition_property { + propertys.push(value); + } else if multiple_items { + // If there is more than one item, and any of transitions has 'none', + // then it's invalid. Othersize, leave propertys to be empty (which + // means "transition-property: none"); + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + % for prop in "duration timing_function delay".split(): + ${prop}s.push(result.transition_${prop}); + % endfor + } + + Ok(expanded! { + % for prop in "property duration timing_function delay".split(): + transition_${prop}: transition_${prop}::SpecifiedValue(${prop}s.into()), + % endfor + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + let property_len = self.transition_property.0.len(); + + // There are two cases that we can do shorthand serialization: + // * when all value lists have the same length, or + // * when transition-property is none, and other value lists have exactly one item. + if property_len == 0 { + % for name in "duration delay timing_function".split(): + if self.transition_${name}.0.len() != 1 { + return Ok(()); + } + % endfor + } else { + % for name in "duration delay timing_function".split(): + if self.transition_${name}.0.len() != property_len { + return Ok(()); + } + % endfor + } + + // Representative length. + let len = self.transition_duration.0.len(); + + for i in 0..len { + if i != 0 { + dest.write_str(", ")?; + } + if property_len == 0 { + dest.write_str("none")?; + } else { + self.transition_property.0[i].to_css(dest)?; + } + % for name in "duration timing_function delay".split(): + dest.write_str(" ")?; + self.transition_${name}.0[i].to_css(dest)?; + % endfor + } + Ok(()) + } + } +</%helpers:shorthand> + +<%helpers:shorthand name="animation" + engines="gecko servo-2013 servo-2020" + extra_prefixes="moz:layout.css.prefixes.animations webkit" + sub_properties="animation-name animation-duration + animation-timing-function animation-delay + animation-iteration-count animation-direction + animation-fill-mode animation-play-state animation-timeline" + rule_types_allowed="Style" + spec="https://drafts.csswg.org/css-animations/#propdef-animation"> + <% + props = "name timeline duration timing_function delay iteration_count \ + direction fill_mode play_state".split() + %> + % for prop in props: + use crate::properties::longhands::animation_${prop}; + % endfor + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + struct SingleAnimation { + % for prop in props: + animation_${prop}: animation_${prop}::SingleSpecifiedValue, + % endfor + } + + fn parse_one_animation<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<SingleAnimation, ParseError<'i>> { + % for prop in props: + let mut ${prop} = None; + % endfor + + let mut parsed = 0; + // NB: Name must be the last one here so that keywords valid for other + // longhands are not interpreted as names. + // + // Also, duration must be before delay, see + // https://drafts.csswg.org/css-animations/#typedef-single-animation + loop { + parsed += 1; + try_parse_one!(context, input, duration, animation_duration); + try_parse_one!(context, input, timing_function, animation_timing_function); + try_parse_one!(context, input, delay, animation_delay); + try_parse_one!(context, input, iteration_count, animation_iteration_count); + try_parse_one!(context, input, direction, animation_direction); + try_parse_one!(context, input, fill_mode, animation_fill_mode); + try_parse_one!(context, input, play_state, animation_play_state); + try_parse_one!(context, input, name, animation_name); + if static_prefs::pref!("layout.css.scroll-driven-animations.enabled") { + try_parse_one!(context, input, timeline, animation_timeline); + } + + parsed -= 1; + break + } + + // If nothing is parsed, this is an invalid entry. + if parsed == 0 { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } else { + Ok(SingleAnimation { + % for prop in props: + animation_${prop}: ${prop}.unwrap_or_else(animation_${prop}::single_value + ::get_initial_specified_value), + % endfor + }) + } + } + + % for prop in props: + let mut ${prop}s = vec![]; + % endfor + + let results = input.parse_comma_separated(|i| parse_one_animation(context, i))?; + for result in results.into_iter() { + % for prop in props: + ${prop}s.push(result.animation_${prop}); + % endfor + } + + Ok(expanded! { + % for prop in props: + animation_${prop}: animation_${prop}::SpecifiedValue(${prop}s.into()), + % endfor + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + let len = self.animation_name.0.len(); + // There should be at least one declared value + if len == 0 { + return Ok(()); + } + + // If any value list length is differs then we don't do a shorthand serialization + // either. + % for name in props[2:]: + if len != self.animation_${name}.0.len() { + return Ok(()) + } + % endfor + + // If the preference of animation-timeline is disabled, `self.animation_timeline` is + // None. + if self.animation_timeline.map_or(false, |v| len != v.0.len()) { + return Ok(()); + } + + for i in 0..len { + if i != 0 { + dest.write_str(", ")?; + } + + % for name in props[2:]: + self.animation_${name}.0[i].to_css(dest)?; + dest.write_str(" ")?; + % endfor + + self.animation_name.0[i].to_css(dest)?; + + // Based on the spec, the default values of other properties must be output in at + // least the cases necessary to distinguish an animation-name. The serialization + // order of animation-timeline is always later than animation-name, so it's fine + // to not serialize it if it is the default value. It's still possible to + // distinguish them (because we always serialize animation-name). + // https://drafts.csswg.org/css-animations-1/#animation + // https://drafts.csswg.org/css-animations-2/#typedef-single-animation + // + // Note: it's also fine to always serialize this. However, it seems Blink + // doesn't serialize default animation-timeline now, so we follow the same rule. + if let Some(ref timeline) = self.animation_timeline { + if !timeline.0[i].is_auto() { + dest.write_char(' ')?; + timeline.0[i].to_css(dest)?; + } + } + } + Ok(()) + } + } +</%helpers:shorthand> + +<%helpers:shorthand + engines="gecko" + name="scroll-timeline" + sub_properties="scroll-timeline-name scroll-timeline-axis" + gecko_pref="layout.css.scroll-driven-animations.enabled", + spec="https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-shorthand" +> + pub fn parse_value<'i>( + context: &ParserContext, + input: &mut Parser<'i, '_>, + ) -> Result<Longhands, ParseError<'i>> { + use crate::parser::Parse; + use crate::values::specified::box_::{ScrollAxis, ScrollTimelineName}; + + let mut name = None; + let mut axis = None; + loop { + // Note: When parsing positionally-ambiguous keywords in a property value, a + // <custom-ident> production can only claim the keyword if no other unfulfilled + // production can claim it. So we try to parse `scroll-timeline-axis` first. + // + // https://drafts.csswg.org/css-values-4/#custom-idents + + if axis.is_none() { + axis = input.try_parse(ScrollAxis::parse).ok(); + } + + if name.is_none() { + if let Ok(value) = input.try_parse(|i| ScrollTimelineName::parse(context, i)) { + name = Some(value); + continue; + } + } + break; + } + + // Must occur one or more. + if name.is_none() && axis.is_none() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(expanded! { + scroll_timeline_name: name.unwrap_or(ScrollTimelineName::none()), + scroll_timeline_axis: axis.unwrap_or_default(), + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + use crate::values::specified::box_::ScrollAxis; + + let is_default_axis = self.scroll_timeline_axis == &ScrollAxis::default(); + let is_default_name = self.scroll_timeline_name.0.is_none(); + + // Note: if both are default values, we serialize the default axis (because it is the + // first value per spec). + if !is_default_axis || (is_default_axis && is_default_name) { + self.scroll_timeline_axis.to_css(dest)?; + } + + if !is_default_name { + if !is_default_axis { + dest.write_char(' ')?; + } + self.scroll_timeline_name.to_css(dest)?; + } + + Ok(()) + } + } +</%helpers:shorthand> |