summaryrefslogtreecommitdiffstats
path: root/servo/components/style/properties/shorthands
diff options
context:
space:
mode:
Diffstat (limited to 'servo/components/style/properties/shorthands')
-rw-r--r--servo/components/style/properties/shorthands/background.mako.rs289
-rw-r--r--servo/components/style/properties/shorthands/border.mako.rs491
-rw-r--r--servo/components/style/properties/shorthands/box.mako.rs253
-rw-r--r--servo/components/style/properties/shorthands/column.mako.rs115
-rw-r--r--servo/components/style/properties/shorthands/font.mako.rs542
-rw-r--r--servo/components/style/properties/shorthands/inherited_svg.mako.rs38
-rw-r--r--servo/components/style/properties/shorthands/inherited_text.mako.rs254
-rw-r--r--servo/components/style/properties/shorthands/list.mako.rs137
-rw-r--r--servo/components/style/properties/shorthands/margin.mako.rs60
-rw-r--r--servo/components/style/properties/shorthands/outline.mako.rs80
-rw-r--r--servo/components/style/properties/shorthands/padding.mako.rs58
-rw-r--r--servo/components/style/properties/shorthands/position.mako.rs891
-rw-r--r--servo/components/style/properties/shorthands/svg.mako.rs287
-rw-r--r--servo/components/style/properties/shorthands/text.mako.rs120
-rw-r--r--servo/components/style/properties/shorthands/ui.mako.rs444
15 files changed, 4059 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..08838233f6
--- /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_char(' ')?;
+ }
+ 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_char(' ')?;
+ }
+
+ 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_char(' ')?;
+ }
+ ${name}.to_css(dest)?;
+ wrote_value = true;
+ }
+ % endfor
+
+ if *origin != Origin::PaddingBox || *clip != Clip::BorderBox {
+ if wrote_value {
+ dest.write_char(' ')?;
+ }
+ origin.to_css(dest)?;
+ if *clip != From::from(*origin) {
+ dest.write_char(' ')?;
+ 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..c6a87f3197
--- /dev/null
+++ b/servo/components/style/properties/shorthands/border.mako.rs
@@ -0,0 +1,491 @@
+/* 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 {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ Ok((color.unwrap_or(Color::CurrentColor), style.unwrap_or(BorderStyle::None), width.unwrap_or(BorderSideWidth::medium())))
+}
+
+% 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 ['width', 'style', 'color']
+ )}"
+ 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 {
+ crate::values::specified::border::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 ['width', 'style', 'color']
+ )}
+ ${' '.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 {
+ return Ok(())
+ }
+ crate::values::specified::border::serialize_directional_border(
+ dest,
+ self.border_${side}_width,
+ self.border_${side}_style,
+ self.border_${side}_color
+ )
+ }
+ }
+
+ // 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 ${name} = border_image_${name}::get_initial_specified_value();
+ % endfor
+ let mut any = false;
+ let mut parsed_slice = false;
+ let mut parsed_source = false;
+ let mut parsed_repeat = false;
+ loop {
+ if !parsed_slice {
+ if let Ok(value) = input.try_parse(|input| border_image_slice::parse(context, input)) {
+ parsed_slice = true;
+ any = true;
+ slice = 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() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ Ok((w, o))
+ });
+ if let Ok((w, o)) = maybe_width_outset {
+ if let Some(w) = w {
+ width = w;
+ }
+ if let Some(o) = o {
+ outset = o;
+ }
+ }
+ continue;
+ }
+ }
+ % for name in "source repeat".split():
+ if !parsed_${name} {
+ if let Ok(value) = input.try_parse(|input| border_image_${name}::parse(context, input)) {
+ ${name} = value;
+ parsed_${name} = true;
+ any = true;
+ continue
+ }
+ }
+ % endfor
+ break
+ }
+ if !any {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ Ok(expanded! {
+ % for name in "outset repeat slice source width".split():
+ border_image_${name}: ${name},
+ % endfor
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ let mut has_any = false;
+ % for name in "source slice outset width repeat".split():
+ let has_${name} = *self.border_image_${name} != border_image_${name}::get_initial_specified_value();
+ has_any = has_any || has_${name};
+ % endfor
+ if has_source || !has_any {
+ self.border_image_source.to_css(dest)?;
+ if !has_any {
+ return Ok(());
+ }
+ }
+ let needs_slice = has_slice || has_width || has_outset;
+ if needs_slice {
+ if has_source {
+ dest.write_char(' ')?;
+ }
+ self.border_image_slice.to_css(dest)?;
+ if has_width || has_outset {
+ dest.write_str(" /")?;
+ if has_width {
+ dest.write_char(' ')?;
+ self.border_image_width.to_css(dest)?;
+ }
+ if has_outset {
+ dest.write_str(" / ")?;
+ self.border_image_outset.to_css(dest)?;
+ }
+ }
+ }
+ if has_repeat {
+ if has_source || needs_slice {
+ dest.write_char(' ')?;
+ }
+ self.border_image_repeat.to_css(dest)?;
+ }
+ Ok(())
+ }
+ }
+</%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_char(' ')?;
+ 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 {
+ crate::values::specified::border::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..f644687dc0
--- /dev/null
+++ b/servo/components/style/properties/shorthands/box.mako.rs
@@ -0,0 +1,253 @@
+/* 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
+ offset-position"
+ spec="https://drafts.fxtf.org/motion-1/#offset-shorthand">
+ use crate::parser::Parse;
+ use crate::values::specified::motion::{OffsetPath, OffsetPosition, OffsetRotate};
+ use crate::values::specified::{LengthPercentage, PositionOrAuto};
+ use crate::Zero;
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ let offset_position =
+ if static_prefs::pref!("layout.css.motion-path-offset-position.enabled") {
+ input.try_parse(|i| OffsetPosition::parse(context, i)).ok()
+ } else {
+ None
+ };
+
+ let offset_path = input.try_parse(|i| OffsetPath::parse(context, i)).ok();
+
+ // Must have one of [offset-position, offset-path].
+ // FIXME: The syntax is out-of-date after the update of the spec.
+ // https://github.com/w3c/fxtf-drafts/issues/515
+ if offset_position.is_none() && offset_path.is_none() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ let mut offset_distance = None;
+ let mut offset_rotate = None;
+ // offset-distance and offset-rotate are grouped with offset-path.
+ if offset_path.is_some() {
+ 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_position: offset_position.unwrap_or(OffsetPosition::normal()),
+ offset_path: offset_path.unwrap_or(OffsetPath::none()),
+ 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 {
+ if let Some(offset_position) = self.offset_position {
+ // The basic concept is: we must serialize offset-position or offset-path group.
+ // offset-path group means "offset-path offset-distance offset-rotate".
+ let must_serialize_path = *self.offset_path != OffsetPath::None
+ || (!self.offset_distance.is_zero() || !self.offset_rotate.is_auto());
+ let position_is_default = matches!(offset_position, OffsetPosition::Normal);
+ if !position_is_default || !must_serialize_path {
+ offset_position.to_css(dest)?;
+ }
+
+ if must_serialize_path {
+ if !position_is_default {
+ dest.write_char(' ')?;
+ }
+ self.offset_path.to_css(dest)?;
+ }
+ } else {
+ // If the pref is off, we always show offset-path.
+ self.offset_path.to_css(dest)?;
+ }
+
+ if !self.offset_distance.is_zero() {
+ dest.write_char(' ')?;
+ self.offset_distance.to_css(dest)?;
+ }
+
+ if !self.offset_rotate.is_auto() {
+ dest.write_char(' ')?;
+ self.offset_rotate.to_css(dest)?;
+ }
+
+ if *self.offset_anchor != PositionOrAuto::auto() {
+ dest.write_str(" / ")?;
+ self.offset_anchor.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..17dcf9d926
--- /dev/null
+++ b/servo/components/style/properties/shorthands/font.mako.rs
@@ -0,0 +1,542 @@
+/* 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::font::LineHeight;
+ use crate::values::specified::{FontSize, FontWeight};
+ 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".split():
+ if self.font_${name} != &font_${name}::get_initial_specified_value() {
+ self.font_${name}.to_css(dest)?;
+ dest.write_char(' ')?;
+ }
+ % endfor
+
+ // The initial specified font-weight value of 'normal' computes as a number (400),
+ // not to the keyword, so we need to check for that as well in order to properly
+ // serialize the computed style.
+ if self.font_weight != &FontWeight::normal() &&
+ self.font_weight != &FontWeight::from_gecko_keyword(400) {
+ self.font_weight.to_css(dest)?;
+ dest.write_char(' ')?;
+ }
+
+ if font_stretch != FontStretchKeyword::Normal {
+ font_stretch.to_css(dest)?;
+ dest.write_char(' ')?;
+ }
+
+ self.font_size.to_css(dest)?;
+
+ if *self.line_height != LineHeight::normal() {
+ dest.write_str(" / ")?;
+ self.line_height.to_css(dest)?;
+ }
+
+ dest.write_char(' ')?;
+ 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">
+% if engine == 'gecko':
+ <% sub_properties = "ligatures caps alternates numeric east_asian position emoji".split() %>
+% else:
+ <% sub_properties = ["caps"] %>
+% endif
+
+% 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_char(' ')?;
+ }
+ has_any = true;
+ value.to_css(dest)?;
+ }
+ }
+ % endfor
+ }
+
+ Ok(())
+ }
+ }
+</%helpers:shorthand>
+
+<%helpers:shorthand name="font-synthesis"
+ engines="gecko"
+ flags="SHORTHAND_IN_GETCS"
+ sub_properties="font-synthesis-weight font-synthesis-style font-synthesis-small-caps font-synthesis-position"
+ derive_value_info="False"
+ spec="https://drafts.csswg.org/css-fonts-3/#propdef-font-variant">
+ <% sub_properties = ["weight", "style", "small_caps", "position"] %>
+
+ use crate::values::specified::FontSynthesis;
+
+ 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} = FontSynthesis::None;
+ % endfor
+
+ if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() {
+ // Leave all the individual values as None
+ } else {
+ let mut has_custom_value = false;
+ while !input.is_exhausted() {
+ try_match_ident_ignore_ascii_case! { input,
+ % for prop in sub_properties:
+ "${prop.replace('_', '-')}" if ${prop} == FontSynthesis::None => {
+ has_custom_value = true;
+ ${prop} = FontSynthesis::Auto;
+ continue;
+ },
+ % endfor
+ }
+ }
+ if !has_custom_value {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ }
+
+ Ok(expanded! {
+ % for prop in sub_properties:
+ font_synthesis_${prop}: ${prop},
+ % endfor
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ let mut has_any = false;
+
+ % for prop in sub_properties:
+ if self.font_synthesis_${prop} == &FontSynthesis::Auto {
+ if has_any {
+ dest.write_char(' ')?;
+ }
+ has_any = true;
+ dest.write_str("${prop.replace('_', '-')}")?;
+ }
+ % endfor
+
+ if !has_any {
+ dest.write_str("none")?;
+ }
+
+ Ok(())
+ }
+ }
+
+ // The shorthand takes the sub-property names of the longhands, and not the
+ // 'auto' keyword like they do, so we can't automatically derive this.
+ impl SpecifiedValueInfo for Longhands {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ f(&[
+ "none",
+ % for prop in sub_properties:
+ "${prop.replace('_', '-')}",
+ % endfor
+ ]);
+ }
+ }
+</%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..f29e78a69f
--- /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://svgwg.org/svg2-draft/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..d470553e42
--- /dev/null
+++ b/servo/components/style/properties/shorthands/inherited_text.mako.rs
@@ -0,0 +1,254 @@
+/* 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>
+
+<%helpers:shorthand
+ name="text-wrap"
+ engines="gecko"
+ sub_properties="text-wrap-mode text-wrap-style"
+ spec="https://www.w3.org/TR/css-text-4/#text-wrap"
+>
+ use crate::properties::longhands::{text_wrap_mode, text_wrap_style};
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ let mut mode = None;
+ let mut style = None;
+
+ loop {
+ if mode.is_none() {
+ if let Ok(value) = input.try_parse(|input| text_wrap_mode::parse(context, input)) {
+ mode = Some(value);
+ continue
+ }
+ }
+ if style.is_none() {
+ if let Ok(value) = input.try_parse(|input| text_wrap_style::parse(context, input)) {
+ style = Some(value);
+ continue
+ }
+ }
+ break
+ }
+ if mode.is_some() || style.is_some() {
+ Ok(expanded! {
+ text_wrap_mode: unwrap_or_initial!(text_wrap_mode, mode),
+ text_wrap_style: unwrap_or_initial!(text_wrap_style, style),
+ })
+ } 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 {
+ use text_wrap_mode::computed_value::T as Mode;
+ use text_wrap_style::computed_value::T as Style;
+
+ if matches!(self.text_wrap_style, None | Some(&Style::Auto)) {
+ return self.text_wrap_mode.to_css(dest);
+ }
+
+ if *self.text_wrap_mode != Mode::Wrap {
+ self.text_wrap_mode.to_css(dest)?;
+ dest.write_char(' ')?;
+ }
+
+ self.text_wrap_style.to_css(dest)
+ }
+ }
+</%helpers:shorthand>
+
+<%helpers:shorthand
+ name="white-space"
+ engines="gecko"
+ sub_properties="text-wrap-mode white-space-collapse"
+ spec="https://www.w3.org/TR/css-text-4/#white-space-property"
+>
+ use crate::properties::longhands::{text_wrap_mode, white_space_collapse};
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ use white_space_collapse::computed_value::T as Collapse;
+ use text_wrap_mode::computed_value::T as Wrap;
+
+ fn parse_special_shorthands<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Longhands, ParseError<'i>> {
+ let (mode, collapse) = try_match_ident_ignore_ascii_case! { input,
+ "normal" => (Wrap::Wrap, Collapse::Collapse),
+ "pre" => (Wrap::Nowrap, Collapse::Preserve),
+ "pre-wrap" => (Wrap::Wrap, Collapse::Preserve),
+ "pre-line" => (Wrap::Wrap, Collapse::PreserveBreaks),
+ // TODO: deprecate/remove -moz-pre-space; the white-space-collapse: preserve-spaces value
+ // should serve this purpose?
+ "-moz-pre-space" => (Wrap::Wrap, Collapse::PreserveSpaces),
+ };
+ Ok(expanded! {
+ text_wrap_mode: mode,
+ white_space_collapse: collapse,
+ })
+ }
+
+ if let Ok(result) = input.try_parse(parse_special_shorthands) {
+ return Ok(result);
+ }
+
+ let mut wrap = None;
+ let mut collapse = None;
+
+ loop {
+ if wrap.is_none() {
+ if let Ok(value) = input.try_parse(|input| text_wrap_mode::parse(context, input)) {
+ wrap = Some(value);
+ continue
+ }
+ }
+ if collapse.is_none() {
+ if let Ok(value) = input.try_parse(|input| white_space_collapse::parse(context, input)) {
+ collapse = Some(value);
+ continue
+ }
+ }
+ break
+ }
+
+ if wrap.is_some() || collapse.is_some() {
+ Ok(expanded! {
+ text_wrap_mode: unwrap_or_initial!(text_wrap_mode, wrap),
+ white_space_collapse: unwrap_or_initial!(white_space_collapse, collapse),
+ })
+ } 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 {
+ use white_space_collapse::computed_value::T as Collapse;
+ use text_wrap_mode::computed_value::T as Wrap;
+
+ match *self.text_wrap_mode {
+ Wrap::Wrap => {
+ match *self.white_space_collapse {
+ Collapse::Collapse => return dest.write_str("normal"),
+ Collapse::Preserve => return dest.write_str("pre-wrap"),
+ Collapse::PreserveBreaks => return dest.write_str("pre-line"),
+ Collapse::PreserveSpaces => return dest.write_str("-moz-pre-space"),
+ _ => (),
+ }
+ },
+ Wrap::Nowrap => {
+ if let Collapse::Preserve = *self.white_space_collapse {
+ return dest.write_str("pre");
+ }
+ },
+ }
+
+ let mut has_value = false;
+ if *self.white_space_collapse != Collapse::Collapse {
+ self.white_space_collapse.to_css(dest)?;
+ has_value = true;
+ }
+
+ if *self.text_wrap_mode != Wrap::Wrap {
+ if has_value {
+ dest.write_char(' ')?;
+ }
+ self.text_wrap_mode.to_css(dest)?;
+ }
+
+ Ok(())
+ }
+ }
+</%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..2e234e3d8f
--- /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_char(' ')?;
+ }
+ 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_char(' ')?;
+ }
+ 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..6ee8ed22c9
--- /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_char(' ')?;
+ }
+ 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..ed7df5e27a
--- /dev/null
+++ b/servo/components/style/properties/shorthands/position.mako.rs
@@ -0,0 +1,891 @@
+/* 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"
+ 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),
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ if *self.flex_direction == flex_direction::get_initial_specified_value() &&
+ *self.flex_wrap != flex_wrap::get_initial_specified_value() {
+ return self.flex_wrap.to_css(dest)
+ }
+ self.flex_direction.to_css(dest)?;
+ if *self.flex_wrap != flex_wrap::get_initial_specified_value() {
+ dest.write_char(' ')?;
+ self.flex_wrap.to_css(dest)?;
+ }
+ Ok(())
+ }
+ }
+</%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_char(' ')?;
+ 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, TemplateAreasParser, 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();
+ let mut areas_parser = TemplateAreasParser::default();
+ if areas_parser.try_parse_string(input).is_ok() {
+ let mut values = vec![];
+ let mut line_names = vec![];
+ line_names.push(first_line_names);
+ 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 areas_parser.try_parse_string(input) {
+ Ok(()) => {
+ 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);
+ }
+ // 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 = areas_parser.finish()
+ .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_char(' ')?;
+ }
+
+ 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_char(' ')?;
+ 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_char(' ')?;
+ 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_char(' ')?;
+ 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_char(' ')?;
+ 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_char(' ')?;
+ 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_char(' ')?;
+ 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..cf34b116ee
--- /dev/null
+++ b/servo/components/style/properties/shorthands/svg.mako.rs
@@ -0,0 +1,287 @@
+/* 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;
+ use style_traits::values::SequenceWriter;
+
+ 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 each <mask-layer>, we serialize it according to the following order:
+ // <mask-layer> =
+ // <mask-reference> ||
+ // <position> [ / <bg-size> ]? ||
+ // <repeat-style> ||
+ // <geometry-box> ||
+ // [ <geometry-box> | no-clip ] ||
+ // <compositing-operator> ||
+ // <masking-mode>
+ // https://drafts.fxtf.org/css-masking-1/#the-mask
+ 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
+
+ let mut has_other = false;
+ % for name in "image mode size repeat composite".split():
+ let has_${name} =
+ *${name} != mask_${name}::single_value::get_initial_specified_value();
+ has_other |= has_${name};
+ % endfor
+ let has_position = *position_x != PositionComponent::zero()
+ || *position_y != PositionComponent::zero();
+ let has_origin = *origin != Origin::BorderBox;
+ let has_clip = *clip != Clip::BorderBox;
+
+ // If all are initial values, we serialize mask-image.
+ if !has_other && !has_position && !has_origin && !has_clip {
+ return image.to_css(dest);
+ }
+
+ let mut writer = SequenceWriter::new(dest, " ");
+ // <mask-reference>
+ if has_image {
+ writer.item(image)?;
+ }
+
+ // <position> [ / <bg-size> ]?
+ if has_position || has_size {
+ writer.item(&Position {
+ horizontal: position_x.clone(),
+ vertical: position_y.clone()
+ })?;
+
+ if has_size {
+ writer.raw_item("/")?;
+ writer.item(size)?;
+ }
+ }
+
+ // <repeat-style>
+ if has_repeat {
+ writer.item(repeat)?;
+ }
+
+ // <geometry-box>
+ if has_origin {
+ writer.item(origin)?;
+ }
+
+ // [ <geometry-box> | no-clip ]
+ if has_clip && *clip != From::from(*origin) {
+ writer.item(clip)?;
+ }
+
+ // <compositing-operator>
+ if has_composite {
+ writer.item(composite)?;
+ }
+
+ // <masking-mode>
+ if has_mode {
+ writer.item(mode)?;
+ }
+ }
+
+ 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..5b071be2c4
--- /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_char(' ')?;
+ }
+ self.text_decoration_thickness.to_css(dest)?;
+ has_value = true;
+ }
+
+ % if engine == "gecko":
+ if !is_solid_style {
+ if has_value {
+ dest.write_char(' ')?;
+ }
+ self.text_decoration_style.to_css(dest)?;
+ has_value = true;
+ }
+
+ if !is_current_color {
+ if has_value {
+ dest.write_char(' ')?;
+ }
+ 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..1fdb5965fc
--- /dev/null
+++ b/servo/components/style/properties/shorthands/ui.mako.rs
@@ -0,0 +1,444 @@
+/* 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 "property duration timing_function delay".split():
+ transition_${prop}: transition_${prop}::SingleSpecifiedValue,
+ % endfor
+ }
+
+ fn parse_one_transition<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ first: bool,
+ ) -> 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(value);
+ continue;
+ }
+
+ // 'none' is not a valid value for <single-transition-property>,
+ // so it's only acceptable as the first item.
+ if first && input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
+ property = Some(TransitionProperty::none());
+ continue;
+ }
+ }
+
+ parsed -= 1;
+ break
+ }
+
+ if parsed != 0 {
+ Ok(SingleTransition {
+ % for prop in "property duration timing_function delay".split():
+ transition_${prop}: ${prop}.unwrap_or_else(transition_${prop}::single_value
+ ::get_initial_specified_value),
+ % endfor
+ })
+ } 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 mut first = true;
+ let mut has_transition_property_none = false;
+ let results = input.parse_comma_separated(|i| {
+ if has_transition_property_none {
+ // If you specify transition-property: none, multiple items are invalid.
+ return Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ let transition = parse_one_transition(context, i, first)?;
+ first = false;
+ has_transition_property_none = transition.transition_property.is_none();
+ Ok(transition)
+ })?;
+ for result in results {
+ % for prop in "property 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 {
+ use crate::Zero;
+ use style_traits::values::SequenceWriter;
+
+ 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(", ")?;
+ }
+
+ let has_duration = !self.transition_duration.0[i].is_zero();
+ let has_timing = !self.transition_timing_function.0[i].is_ease();
+ let has_delay = !self.transition_delay.0[i].is_zero();
+ let has_any = has_duration || has_timing || has_delay;
+
+ let mut writer = SequenceWriter::new(dest, " ");
+
+ if property_len == 0 {
+ writer.raw_item("none")?;
+ } else if !self.transition_property.0[i].is_all() || !has_any {
+ writer.item(&self.transition_property.0[i])?;
+ }
+
+ // In order to avoid ambiguity, we have to serialize duration if we have delay.
+ if has_duration || has_delay {
+ writer.item(&self.transition_duration.0[i])?;
+ }
+
+ if has_timing {
+ writer.item(&self.transition_timing_function.0[i])?;
+ }
+
+ if has_delay {
+ writer.item(&self.transition_delay.0[i])?;
+ }
+ }
+ 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_char(' ')?;
+ % 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::properties::longhands::{scroll_timeline_axis, scroll_timeline_name};
+
+ let mut names = Vec::with_capacity(1);
+ let mut axes = Vec::with_capacity(1);
+ input.parse_comma_separated(|input| {
+ let name = scroll_timeline_name::single_value::parse(context, input)?;
+ let axis = input.try_parse(|i| scroll_timeline_axis::single_value::parse(context, i));
+
+ names.push(name);
+ axes.push(axis.unwrap_or_default());
+
+ Ok(())
+ })?;
+
+ Ok(expanded! {
+ scroll_timeline_name: scroll_timeline_name::SpecifiedValue(names.into()),
+ scroll_timeline_axis: scroll_timeline_axis::SpecifiedValue(axes.into()),
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ // If any value list length is differs then we don't do a shorthand serialization
+ // either.
+ let len = self.scroll_timeline_name.0.len();
+ if len != self.scroll_timeline_axis.0.len() {
+ return Ok(());
+ }
+
+ for i in 0..len {
+ if i != 0 {
+ dest.write_str(", ")?;
+ }
+
+ self.scroll_timeline_name.0[i].to_css(dest)?;
+
+ if self.scroll_timeline_axis.0[i] != Default::default() {
+ dest.write_char(' ')?;
+ self.scroll_timeline_axis.0[i].to_css(dest)?;
+ }
+
+ }
+ Ok(())
+ }
+ }
+</%helpers:shorthand>
+
+// Note: view-timeline shorthand doesn't take view-timeline-inset into account.
+<%helpers:shorthand
+ engines="gecko"
+ name="view-timeline"
+ sub_properties="view-timeline-name view-timeline-axis"
+ gecko_pref="layout.css.scroll-driven-animations.enabled",
+ spec="https://drafts.csswg.org/scroll-animations-1/#view-timeline-shorthand"
+>
+ pub fn parse_value<'i>(
+ context: &ParserContext,
+ input: &mut Parser<'i, '_>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ use crate::properties::longhands::{view_timeline_axis, view_timeline_name};
+
+ let mut names = Vec::with_capacity(1);
+ let mut axes = Vec::with_capacity(1);
+ input.parse_comma_separated(|input| {
+ let name = view_timeline_name::single_value::parse(context, input)?;
+ let axis = input.try_parse(|i| view_timeline_axis::single_value::parse(context, i));
+
+ names.push(name);
+ axes.push(axis.unwrap_or_default());
+
+ Ok(())
+ })?;
+
+ Ok(expanded! {
+ view_timeline_name: view_timeline_name::SpecifiedValue(names.into()),
+ view_timeline_axis: view_timeline_axis::SpecifiedValue(axes.into()),
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ // If any value list length is differs then we don't do a shorthand serialization
+ // either.
+ let len = self.view_timeline_name.0.len();
+ if len != self.view_timeline_axis.0.len() {
+ return Ok(());
+ }
+
+ for i in 0..len {
+ if i != 0 {
+ dest.write_str(", ")?;
+ }
+
+ self.view_timeline_name.0[i].to_css(dest)?;
+
+ if self.view_timeline_axis.0[i] != Default::default() {
+ dest.write_char(' ')?;
+ self.view_timeline_axis.0[i].to_css(dest)?;
+ }
+
+ }
+ Ok(())
+ }
+ }
+</%helpers:shorthand>