diff options
Diffstat (limited to 'servo/components/style/values/specified')
34 files changed, 17270 insertions, 0 deletions
diff --git a/servo/components/style/values/specified/align.rs b/servo/components/style/values/specified/align.rs new file mode 100644 index 0000000000..ebb6f63834 --- /dev/null +++ b/servo/components/style/values/specified/align.rs @@ -0,0 +1,817 @@ +/* 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/. */ + +//! Values for CSS Box Alignment properties +//! +//! https://drafts.csswg.org/css-align/ + +use crate::parser::{Parse, ParserContext}; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, KeywordsCollectFn, ParseError, SpecifiedValueInfo, ToCss}; + +bitflags! { + /// Constants shared by multiple CSS Box Alignment properties + #[derive(MallocSizeOf, ToComputedValue, ToResolvedValue, ToShmem)] + #[repr(C)] + pub struct AlignFlags: u8 { + // Enumeration stored in the lower 5 bits: + /// {align,justify}-{content,items,self}: 'auto' + const AUTO = 0; + /// 'normal' + const NORMAL = 1; + /// 'start' + const START = 2; + /// 'end' + const END = 3; + /// 'flex-start' + const FLEX_START = 4; + /// 'flex-end' + const FLEX_END = 5; + /// 'center' + const CENTER = 6; + /// 'left' + const LEFT = 7; + /// 'right' + const RIGHT = 8; + /// 'baseline' + const BASELINE = 9; + /// 'last-baseline' + const LAST_BASELINE = 10; + /// 'stretch' + const STRETCH = 11; + /// 'self-start' + const SELF_START = 12; + /// 'self-end' + const SELF_END = 13; + /// 'space-between' + const SPACE_BETWEEN = 14; + /// 'space-around' + const SPACE_AROUND = 15; + /// 'space-evenly' + const SPACE_EVENLY = 16; + + // Additional flags stored in the upper bits: + /// 'legacy' (mutually exclusive w. SAFE & UNSAFE) + const LEGACY = 1 << 5; + /// 'safe' + const SAFE = 1 << 6; + /// 'unsafe' (mutually exclusive w. SAFE) + const UNSAFE = 1 << 7; + + /// Mask for the additional flags above. + const FLAG_BITS = 0b11100000; + } +} + +impl AlignFlags { + /// Returns the enumeration value stored in the lower 5 bits. + #[inline] + fn value(&self) -> Self { + *self & !AlignFlags::FLAG_BITS + } +} + +impl ToCss for AlignFlags { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + let extra_flags = *self & AlignFlags::FLAG_BITS; + let value = self.value(); + + match extra_flags { + AlignFlags::LEGACY => { + dest.write_str("legacy")?; + if value.is_empty() { + return Ok(()); + } + dest.write_char(' ')?; + }, + AlignFlags::SAFE => dest.write_str("safe ")?, + AlignFlags::UNSAFE => dest.write_str("unsafe ")?, + _ => { + debug_assert_eq!(extra_flags, AlignFlags::empty()); + }, + } + + dest.write_str(match value { + AlignFlags::AUTO => "auto", + AlignFlags::NORMAL => "normal", + AlignFlags::START => "start", + AlignFlags::END => "end", + AlignFlags::FLEX_START => "flex-start", + AlignFlags::FLEX_END => "flex-end", + AlignFlags::CENTER => "center", + AlignFlags::LEFT => "left", + AlignFlags::RIGHT => "right", + AlignFlags::BASELINE => "baseline", + AlignFlags::LAST_BASELINE => "last baseline", + AlignFlags::STRETCH => "stretch", + AlignFlags::SELF_START => "self-start", + AlignFlags::SELF_END => "self-end", + AlignFlags::SPACE_BETWEEN => "space-between", + AlignFlags::SPACE_AROUND => "space-around", + AlignFlags::SPACE_EVENLY => "space-evenly", + _ => unreachable!(), + }) + } +} + +/// An axis direction, either inline (for the `justify` properties) or block, +/// (for the `align` properties). +#[derive(Clone, Copy, PartialEq)] +pub enum AxisDirection { + /// Block direction. + Block, + /// Inline direction. + Inline, +} + +/// Shared value for the `align-content` and `justify-content` properties. +/// +/// <https://drafts.csswg.org/css-align/#content-distribution> +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +pub struct ContentDistribution { + primary: AlignFlags, + // FIXME(https://github.com/w3c/csswg-drafts/issues/1002): This will need to + // accept fallback alignment, eventually. +} + +impl ContentDistribution { + /// The initial value 'normal' + #[inline] + pub fn normal() -> Self { + Self::new(AlignFlags::NORMAL) + } + + /// `start` + #[inline] + pub fn start() -> Self { + Self::new(AlignFlags::START) + } + + /// The initial value 'normal' + #[inline] + pub fn new(primary: AlignFlags) -> Self { + Self { primary } + } + + /// Returns whether this value is a <baseline-position>. + pub fn is_baseline_position(&self) -> bool { + matches!( + self.primary.value(), + AlignFlags::BASELINE | AlignFlags::LAST_BASELINE + ) + } + + /// The primary alignment + #[inline] + pub fn primary(self) -> AlignFlags { + self.primary + } + + /// Parse a value for align-content / justify-content. + pub fn parse<'i, 't>( + input: &mut Parser<'i, 't>, + axis: AxisDirection, + ) -> Result<Self, ParseError<'i>> { + // NOTE Please also update the `list_keywords` function below + // when this function is updated. + + // Try to parse normal first + if input + .try_parse(|i| i.expect_ident_matching("normal")) + .is_ok() + { + return Ok(ContentDistribution::normal()); + } + + // Parse <baseline-position>, but only on the block axis. + if axis == AxisDirection::Block { + if let Ok(value) = input.try_parse(parse_baseline) { + return Ok(ContentDistribution::new(value)); + } + } + + // <content-distribution> + if let Ok(value) = input.try_parse(parse_content_distribution) { + return Ok(ContentDistribution::new(value)); + } + + // <overflow-position>? <content-position> + let overflow_position = input + .try_parse(parse_overflow_position) + .unwrap_or(AlignFlags::empty()); + + let content_position = try_match_ident_ignore_ascii_case! { input, + "start" => AlignFlags::START, + "end" => AlignFlags::END, + "flex-start" => AlignFlags::FLEX_START, + "flex-end" => AlignFlags::FLEX_END, + "center" => AlignFlags::CENTER, + "left" if axis == AxisDirection::Inline => AlignFlags::LEFT, + "right" if axis == AxisDirection::Inline => AlignFlags::RIGHT, + }; + + Ok(ContentDistribution::new( + content_position | overflow_position, + )) + } + + fn list_keywords(f: KeywordsCollectFn, axis: AxisDirection) { + f(&["normal"]); + if axis == AxisDirection::Block { + list_baseline_keywords(f); + } + list_content_distribution_keywords(f); + list_overflow_position_keywords(f); + f(&["start", "end", "flex-start", "flex-end", "center"]); + if axis == AxisDirection::Inline { + f(&["left", "right"]); + } + } +} + +/// Value for the `align-content` property. +/// +/// <https://drafts.csswg.org/css-align/#propdef-align-content> +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct AlignContent(pub ContentDistribution); + +impl Parse for AlignContent { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // NOTE Please also update `impl SpecifiedValueInfo` below when + // this function is updated. + Ok(AlignContent(ContentDistribution::parse( + input, + AxisDirection::Block, + )?)) + } +} + +impl SpecifiedValueInfo for AlignContent { + fn collect_completion_keywords(f: KeywordsCollectFn) { + ContentDistribution::list_keywords(f, AxisDirection::Block); + } +} + +/// Value for the `align-tracks` property. +/// +/// <https://github.com/w3c/csswg-drafts/issues/4650> +#[derive( + Clone, + Debug, + Default, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +#[css(comma)] +pub struct AlignTracks(#[css(iterable, if_empty = "normal")] pub crate::OwnedSlice<AlignContent>); + +impl Parse for AlignTracks { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let values = input.parse_comma_separated(|input| AlignContent::parse(context, input))?; + Ok(AlignTracks(values.into())) + } +} + +/// Value for the `justify-content` property. +/// +/// <https://drafts.csswg.org/css-align/#propdef-justify-content> +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct JustifyContent(pub ContentDistribution); + +impl Parse for JustifyContent { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // NOTE Please also update `impl SpecifiedValueInfo` below when + // this function is updated. + Ok(JustifyContent(ContentDistribution::parse( + input, + AxisDirection::Inline, + )?)) + } +} + +impl SpecifiedValueInfo for JustifyContent { + fn collect_completion_keywords(f: KeywordsCollectFn) { + ContentDistribution::list_keywords(f, AxisDirection::Inline); + } +} +/// Value for the `justify-tracks` property. +/// +/// <https://github.com/w3c/csswg-drafts/issues/4650> +#[derive( + Clone, + Debug, + Default, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +#[css(comma)] +pub struct JustifyTracks( + #[css(iterable, if_empty = "normal")] pub crate::OwnedSlice<JustifyContent>, +); + +impl Parse for JustifyTracks { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let values = input.parse_comma_separated(|input| JustifyContent::parse(context, input))?; + Ok(JustifyTracks(values.into())) + } +} + +/// <https://drafts.csswg.org/css-align/#self-alignment> +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct SelfAlignment(pub AlignFlags); + +impl SelfAlignment { + /// The initial value 'auto' + #[inline] + pub fn auto() -> Self { + SelfAlignment(AlignFlags::AUTO) + } + + /// Returns whether this value is valid for both axis directions. + pub fn is_valid_on_both_axes(&self) -> bool { + match self.0.value() { + // left | right are only allowed on the inline axis. + AlignFlags::LEFT | AlignFlags::RIGHT => false, + + _ => true, + } + } + + /// Parse a self-alignment value on one of the axis. + pub fn parse<'i, 't>( + input: &mut Parser<'i, 't>, + axis: AxisDirection, + ) -> Result<Self, ParseError<'i>> { + // NOTE Please also update the `list_keywords` function below + // when this function is updated. + + // <baseline-position> + // + // It's weird that this accepts <baseline-position>, but not + // justify-content... + if let Ok(value) = input.try_parse(parse_baseline) { + return Ok(SelfAlignment(value)); + } + + // auto | normal | stretch + if let Ok(value) = input.try_parse(parse_auto_normal_stretch) { + return Ok(SelfAlignment(value)); + } + + // <overflow-position>? <self-position> + let overflow_position = input + .try_parse(parse_overflow_position) + .unwrap_or(AlignFlags::empty()); + let self_position = parse_self_position(input, axis)?; + Ok(SelfAlignment(overflow_position | self_position)) + } + + fn list_keywords(f: KeywordsCollectFn, axis: AxisDirection) { + list_baseline_keywords(f); + list_auto_normal_stretch(f); + list_overflow_position_keywords(f); + list_self_position_keywords(f, axis); + } +} + +/// The specified value of the align-self property. +/// +/// <https://drafts.csswg.org/css-align/#propdef-align-self> +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct AlignSelf(pub SelfAlignment); + +impl Parse for AlignSelf { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // NOTE Please also update `impl SpecifiedValueInfo` below when + // this function is updated. + Ok(AlignSelf(SelfAlignment::parse( + input, + AxisDirection::Block, + )?)) + } +} + +impl SpecifiedValueInfo for AlignSelf { + fn collect_completion_keywords(f: KeywordsCollectFn) { + SelfAlignment::list_keywords(f, AxisDirection::Block); + } +} + +/// The specified value of the justify-self property. +/// +/// <https://drafts.csswg.org/css-align/#propdef-justify-self> +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct JustifySelf(pub SelfAlignment); + +impl Parse for JustifySelf { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // NOTE Please also update `impl SpecifiedValueInfo` below when + // this function is updated. + Ok(JustifySelf(SelfAlignment::parse( + input, + AxisDirection::Inline, + )?)) + } +} + +impl SpecifiedValueInfo for JustifySelf { + fn collect_completion_keywords(f: KeywordsCollectFn) { + SelfAlignment::list_keywords(f, AxisDirection::Inline); + } +} + +/// Value of the `align-items` property +/// +/// <https://drafts.csswg.org/css-align/#propdef-align-items> +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct AlignItems(pub AlignFlags); + +impl AlignItems { + /// The initial value 'normal' + #[inline] + pub fn normal() -> Self { + AlignItems(AlignFlags::NORMAL) + } +} + +impl Parse for AlignItems { + // normal | stretch | <baseline-position> | + // <overflow-position>? <self-position> + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // NOTE Please also update `impl SpecifiedValueInfo` below when + // this function is updated. + + // <baseline-position> + if let Ok(baseline) = input.try_parse(parse_baseline) { + return Ok(AlignItems(baseline)); + } + + // normal | stretch + if let Ok(value) = input.try_parse(parse_normal_stretch) { + return Ok(AlignItems(value)); + } + // <overflow-position>? <self-position> + let overflow = input + .try_parse(parse_overflow_position) + .unwrap_or(AlignFlags::empty()); + let self_position = parse_self_position(input, AxisDirection::Block)?; + Ok(AlignItems(self_position | overflow)) + } +} + +impl SpecifiedValueInfo for AlignItems { + fn collect_completion_keywords(f: KeywordsCollectFn) { + list_baseline_keywords(f); + list_normal_stretch(f); + list_overflow_position_keywords(f); + list_self_position_keywords(f, AxisDirection::Block); + } +} + +/// Value of the `justify-items` property +/// +/// <https://drafts.csswg.org/css-align/#justify-items-property> +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToCss, ToResolvedValue, ToShmem)] +#[repr(C)] +pub struct JustifyItems(pub AlignFlags); + +impl JustifyItems { + /// The initial value 'legacy' + #[inline] + pub fn legacy() -> Self { + JustifyItems(AlignFlags::LEGACY) + } + + /// The value 'normal' + #[inline] + pub fn normal() -> Self { + JustifyItems(AlignFlags::NORMAL) + } +} + +impl Parse for JustifyItems { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // NOTE Please also update `impl SpecifiedValueInfo` below when + // this function is updated. + + // <baseline-position> + // + // It's weird that this accepts <baseline-position>, but not + // justify-content... + if let Ok(baseline) = input.try_parse(parse_baseline) { + return Ok(JustifyItems(baseline)); + } + + // normal | stretch + if let Ok(value) = input.try_parse(parse_normal_stretch) { + return Ok(JustifyItems(value)); + } + + // legacy | [ legacy && [ left | right | center ] ] + if let Ok(value) = input.try_parse(parse_legacy) { + return Ok(JustifyItems(value)); + } + + // <overflow-position>? <self-position> + let overflow = input + .try_parse(parse_overflow_position) + .unwrap_or(AlignFlags::empty()); + let self_position = parse_self_position(input, AxisDirection::Inline)?; + Ok(JustifyItems(overflow | self_position)) + } +} + +impl SpecifiedValueInfo for JustifyItems { + fn collect_completion_keywords(f: KeywordsCollectFn) { + list_baseline_keywords(f); + list_normal_stretch(f); + list_legacy_keywords(f); + list_overflow_position_keywords(f); + list_self_position_keywords(f, AxisDirection::Inline); + } +} + +// auto | normal | stretch +fn parse_auto_normal_stretch<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<AlignFlags, ParseError<'i>> { + // NOTE Please also update the `list_auto_normal_stretch` function + // below when this function is updated. + try_match_ident_ignore_ascii_case! { input, + "auto" => Ok(AlignFlags::AUTO), + "normal" => Ok(AlignFlags::NORMAL), + "stretch" => Ok(AlignFlags::STRETCH), + } +} + +fn list_auto_normal_stretch(f: KeywordsCollectFn) { + f(&["auto", "normal", "stretch"]); +} + +// normal | stretch +fn parse_normal_stretch<'i, 't>(input: &mut Parser<'i, 't>) -> Result<AlignFlags, ParseError<'i>> { + // NOTE Please also update the `list_normal_stretch` function below + // when this function is updated. + try_match_ident_ignore_ascii_case! { input, + "normal" => Ok(AlignFlags::NORMAL), + "stretch" => Ok(AlignFlags::STRETCH), + } +} + +fn list_normal_stretch(f: KeywordsCollectFn) { + f(&["normal", "stretch"]); +} + +// <baseline-position> +fn parse_baseline<'i, 't>(input: &mut Parser<'i, 't>) -> Result<AlignFlags, ParseError<'i>> { + // NOTE Please also update the `list_baseline_keywords` function + // below when this function is updated. + try_match_ident_ignore_ascii_case! { input, + "baseline" => Ok(AlignFlags::BASELINE), + "first" => { + input.expect_ident_matching("baseline")?; + Ok(AlignFlags::BASELINE) + }, + "last" => { + input.expect_ident_matching("baseline")?; + Ok(AlignFlags::LAST_BASELINE) + }, + } +} + +fn list_baseline_keywords(f: KeywordsCollectFn) { + f(&["baseline", "first baseline", "last baseline"]); +} + +// <content-distribution> +fn parse_content_distribution<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<AlignFlags, ParseError<'i>> { + // NOTE Please also update the `list_content_distribution_keywords` + // function below when this function is updated. + try_match_ident_ignore_ascii_case! { input, + "stretch" => Ok(AlignFlags::STRETCH), + "space-between" => Ok(AlignFlags::SPACE_BETWEEN), + "space-around" => Ok(AlignFlags::SPACE_AROUND), + "space-evenly" => Ok(AlignFlags::SPACE_EVENLY), + } +} + +fn list_content_distribution_keywords(f: KeywordsCollectFn) { + f(&["stretch", "space-between", "space-around", "space-evenly"]); +} + +// <overflow-position> +fn parse_overflow_position<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<AlignFlags, ParseError<'i>> { + // NOTE Please also update the `list_overflow_position_keywords` + // function below when this function is updated. + try_match_ident_ignore_ascii_case! { input, + "safe" => Ok(AlignFlags::SAFE), + "unsafe" => Ok(AlignFlags::UNSAFE), + } +} + +fn list_overflow_position_keywords(f: KeywordsCollectFn) { + f(&["safe", "unsafe"]); +} + +// <self-position> | left | right in the inline axis. +fn parse_self_position<'i, 't>( + input: &mut Parser<'i, 't>, + axis: AxisDirection, +) -> Result<AlignFlags, ParseError<'i>> { + // NOTE Please also update the `list_self_position_keywords` + // function below when this function is updated. + Ok(try_match_ident_ignore_ascii_case! { input, + "start" => AlignFlags::START, + "end" => AlignFlags::END, + "flex-start" => AlignFlags::FLEX_START, + "flex-end" => AlignFlags::FLEX_END, + "center" => AlignFlags::CENTER, + "self-start" => AlignFlags::SELF_START, + "self-end" => AlignFlags::SELF_END, + "left" if axis == AxisDirection::Inline => AlignFlags::LEFT, + "right" if axis == AxisDirection::Inline => AlignFlags::RIGHT, + }) +} + +fn list_self_position_keywords(f: KeywordsCollectFn, axis: AxisDirection) { + f(&[ + "start", + "end", + "flex-start", + "flex-end", + "center", + "self-start", + "self-end", + ]); + if axis == AxisDirection::Inline { + f(&["left", "right"]); + } +} + +fn parse_left_right_center<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<AlignFlags, ParseError<'i>> { + // NOTE Please also update the `list_legacy_keywords` function below + // when this function is updated. + Ok(try_match_ident_ignore_ascii_case! { input, + "left" => AlignFlags::LEFT, + "right" => AlignFlags::RIGHT, + "center" => AlignFlags::CENTER, + }) +} + +// legacy | [ legacy && [ left | right | center ] ] +fn parse_legacy<'i, 't>(input: &mut Parser<'i, 't>) -> Result<AlignFlags, ParseError<'i>> { + // NOTE Please also update the `list_legacy_keywords` function below + // when this function is updated. + let flags = try_match_ident_ignore_ascii_case! { input, + "legacy" => { + let flags = input.try_parse(parse_left_right_center) + .unwrap_or(AlignFlags::empty()); + + return Ok(AlignFlags::LEGACY | flags) + }, + "left" => AlignFlags::LEFT, + "right" => AlignFlags::RIGHT, + "center" => AlignFlags::CENTER, + }; + + input.expect_ident_matching("legacy")?; + Ok(AlignFlags::LEGACY | flags) +} + +fn list_legacy_keywords(f: KeywordsCollectFn) { + f(&["legacy", "left", "right", "center"]); +} diff --git a/servo/components/style/values/specified/angle.rs b/servo/components/style/values/specified/angle.rs new file mode 100644 index 0000000000..fa60f66507 --- /dev/null +++ b/servo/components/style/values/specified/angle.rs @@ -0,0 +1,240 @@ +/* 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/. */ + +//! Specified angles. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::angle::Angle as ComputedAngle; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::specified::calc::CalcNode; +use crate::values::CSSFloat; +use crate::Zero; +use cssparser::{Parser, Token}; +use std::f32::consts::PI; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, ToCss}; + +/// A specified angle dimension. +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, ToCss, ToShmem)] +pub enum AngleDimension { + /// An angle with degree unit. + #[css(dimension)] + Deg(CSSFloat), + /// An angle with gradian unit. + #[css(dimension)] + Grad(CSSFloat), + /// An angle with radian unit. + #[css(dimension)] + Rad(CSSFloat), + /// An angle with turn unit. + #[css(dimension)] + Turn(CSSFloat), +} + +impl Zero for AngleDimension { + fn zero() -> Self { + AngleDimension::Deg(0.) + } + + fn is_zero(&self) -> bool { + match *self { + AngleDimension::Deg(ref f) | + AngleDimension::Grad(ref f) | + AngleDimension::Rad(ref f) | + AngleDimension::Turn(ref f) => *f == 0., + } + } +} + +impl AngleDimension { + /// Returns the amount of degrees this angle represents. + #[inline] + fn degrees(&self) -> CSSFloat { + const DEG_PER_RAD: f32 = 180.0 / PI; + const DEG_PER_TURN: f32 = 360.0; + const DEG_PER_GRAD: f32 = 180.0 / 200.0; + + match *self { + AngleDimension::Deg(d) => d, + AngleDimension::Rad(rad) => rad * DEG_PER_RAD, + AngleDimension::Turn(turns) => turns * DEG_PER_TURN, + AngleDimension::Grad(gradians) => gradians * DEG_PER_GRAD, + } + } +} + +/// A specified Angle value, which is just the angle dimension, plus whether it +/// was specified as `calc()` or not. +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub struct Angle { + value: AngleDimension, + was_calc: bool, +} + +impl Zero for Angle { + fn zero() -> Self { + Self { + value: Zero::zero(), + was_calc: false, + } + } + + fn is_zero(&self) -> bool { + self.value.is_zero() + } +} + +impl ToCss for Angle { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.was_calc { + dest.write_str("calc(")?; + } + self.value.to_css(dest)?; + if self.was_calc { + dest.write_str(")")?; + } + Ok(()) + } +} + +impl ToComputedValue for Angle { + type ComputedValue = ComputedAngle; + + #[inline] + fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue { + ComputedAngle::from_degrees(self.degrees()) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Angle { + value: AngleDimension::Deg(computed.degrees()), + was_calc: false, + } + } +} + +impl Angle { + /// Creates an angle with the given value in degrees. + #[inline] + pub fn from_degrees(value: CSSFloat, was_calc: bool) -> Self { + Angle { + value: AngleDimension::Deg(value), + was_calc, + } + } + + /// Return `0deg`. + pub fn zero() -> Self { + Self::from_degrees(0.0, false) + } + + /// Returns the value of the angle in degrees, mostly for `calc()`. + #[inline] + pub fn degrees(&self) -> CSSFloat { + self.value.degrees() + } + + /// Whether this specified angle came from a `calc()` expression. + #[inline] + pub fn was_calc(&self) -> bool { + self.was_calc + } + + /// Returns an `Angle` parsed from a `calc()` expression. + pub fn from_calc(degrees: CSSFloat) -> Self { + Angle { + value: AngleDimension::Deg(degrees), + was_calc: true, + } + } +} + +/// Whether to allow parsing an unitless zero as a valid angle. +/// +/// This should always be `No`, except for exceptions like: +/// +/// https://github.com/w3c/fxtf-drafts/issues/228 +/// +/// See also: https://github.com/w3c/csswg-drafts/issues/1162. +#[allow(missing_docs)] +pub enum AllowUnitlessZeroAngle { + Yes, + No, +} + +impl Parse for Angle { + /// Parses an angle according to CSS-VALUES § 6.1. + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal(context, input, AllowUnitlessZeroAngle::No) + } +} + +impl Angle { + /// Parse an `<angle>` value given a value and an unit. + pub fn parse_dimension(value: CSSFloat, unit: &str, was_calc: bool) -> Result<Angle, ()> { + let value = match_ignore_ascii_case! { unit, + "deg" => AngleDimension::Deg(value), + "grad" => AngleDimension::Grad(value), + "turn" => AngleDimension::Turn(value), + "rad" => AngleDimension::Rad(value), + _ => return Err(()) + }; + + Ok(Self { value, was_calc }) + } + + /// Parse an `<angle>` allowing unitless zero to represent a zero angle. + /// + /// See the comment in `AllowUnitlessZeroAngle` for why. + #[inline] + pub fn parse_with_unitless<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal(context, input, AllowUnitlessZeroAngle::Yes) + } + + pub(super) fn parse_internal<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_unitless_zero: AllowUnitlessZeroAngle, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let t = input.next()?; + let allow_unitless_zero = matches!(allow_unitless_zero, AllowUnitlessZeroAngle::Yes); + match *t { + Token::Dimension { + value, ref unit, .. + } => { + match Angle::parse_dimension(value, unit, /* from_calc = */ false) { + Ok(angle) => Ok(angle), + Err(()) => { + let t = t.clone(); + Err(input.new_unexpected_token_error(t)) + }, + } + }, + Token::Function(ref name) => { + let function = CalcNode::math_function(name, location)?; + CalcNode::parse_angle(context, input, function) + }, + Token::Number { value, .. } if value == 0. && allow_unitless_zero => Ok(Angle::zero()), + ref t => { + let t = t.clone(); + Err(input.new_unexpected_token_error(t)) + }, + } + } +} + +impl SpecifiedValueInfo for Angle {} diff --git a/servo/components/style/values/specified/background.rs b/servo/components/style/values/specified/background.rs new file mode 100644 index 0000000000..29f6d6b352 --- /dev/null +++ b/servo/components/style/values/specified/background.rs @@ -0,0 +1,143 @@ +/* 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/. */ + +//! Specified types for CSS values related to backgrounds. + +use crate::parser::{Parse, ParserContext}; +use crate::values::generics::background::BackgroundSize as GenericBackgroundSize; +use crate::values::specified::length::{ + NonNegativeLengthPercentage, NonNegativeLengthPercentageOrAuto, +}; +use cssparser::Parser; +use selectors::parser::SelectorParseErrorKind; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, ToCss}; + +/// A specified value for the `background-size` property. +pub type BackgroundSize = GenericBackgroundSize<NonNegativeLengthPercentage>; + +impl Parse for BackgroundSize { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(width) = input.try_parse(|i| NonNegativeLengthPercentageOrAuto::parse(context, i)) + { + let height = input + .try_parse(|i| NonNegativeLengthPercentageOrAuto::parse(context, i)) + .unwrap_or(NonNegativeLengthPercentageOrAuto::auto()); + return Ok(GenericBackgroundSize::ExplicitSize { width, height }); + } + Ok(try_match_ident_ignore_ascii_case! { input, + "cover" => GenericBackgroundSize::Cover, + "contain" => GenericBackgroundSize::Contain, + }) + } +} + +/// One of the keywords for `background-repeat`. +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +#[value_info(other_values = "repeat-x,repeat-y")] +pub enum BackgroundRepeatKeyword { + Repeat, + Space, + Round, + NoRepeat, +} + +/// The value of the `background-repeat` property, with `repeat-x` / `repeat-y` +/// represented as the combination of `no-repeat` and `repeat` in the opposite +/// axes. +/// +/// https://drafts.csswg.org/css-backgrounds/#the-background-repeat +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +pub struct BackgroundRepeat(pub BackgroundRepeatKeyword, pub BackgroundRepeatKeyword); + +impl BackgroundRepeat { + /// Returns the `repeat repeat` value. + pub fn repeat() -> Self { + BackgroundRepeat( + BackgroundRepeatKeyword::Repeat, + BackgroundRepeatKeyword::Repeat, + ) + } +} + +impl ToCss for BackgroundRepeat { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match (self.0, self.1) { + (BackgroundRepeatKeyword::Repeat, BackgroundRepeatKeyword::NoRepeat) => { + dest.write_str("repeat-x") + }, + (BackgroundRepeatKeyword::NoRepeat, BackgroundRepeatKeyword::Repeat) => { + dest.write_str("repeat-y") + }, + (horizontal, vertical) => { + horizontal.to_css(dest)?; + if horizontal != vertical { + dest.write_str(" ")?; + vertical.to_css(dest)?; + } + Ok(()) + }, + } + } +} + +impl Parse for BackgroundRepeat { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let ident = input.expect_ident_cloned()?; + + match_ignore_ascii_case! { &ident, + "repeat-x" => { + return Ok(BackgroundRepeat(BackgroundRepeatKeyword::Repeat, BackgroundRepeatKeyword::NoRepeat)); + }, + "repeat-y" => { + return Ok(BackgroundRepeat(BackgroundRepeatKeyword::NoRepeat, BackgroundRepeatKeyword::Repeat)); + }, + _ => {}, + } + + let horizontal = match BackgroundRepeatKeyword::from_ident(&ident) { + Ok(h) => h, + Err(()) => { + return Err( + input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone())) + ); + }, + }; + + let vertical = input.try_parse(BackgroundRepeatKeyword::parse).ok(); + Ok(BackgroundRepeat(horizontal, vertical.unwrap_or(horizontal))) + } +} diff --git a/servo/components/style/values/specified/basic_shape.rs b/servo/components/style/values/specified/basic_shape.rs new file mode 100644 index 0000000000..37aaae2ec2 --- /dev/null +++ b/servo/components/style/values/specified/basic_shape.rs @@ -0,0 +1,332 @@ +/* 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/. */ + +//! CSS handling for the specified value of +//! [`basic-shape`][basic-shape]s +//! +//! [basic-shape]: https://drafts.csswg.org/css-shapes/#typedef-basic-shape + +use crate::parser::{Parse, ParserContext}; +use crate::values::generics::basic_shape as generic; +use crate::values::generics::basic_shape::{Path, PolygonCoord}; +use crate::values::generics::rect::Rect; +use crate::values::specified::border::BorderRadius; +use crate::values::specified::image::Image; +use crate::values::specified::position::{HorizontalPosition, Position, VerticalPosition}; +use crate::values::specified::url::SpecifiedUrl; +use crate::values::specified::SVGPathData; +use crate::values::specified::{LengthPercentage, NonNegativeLengthPercentage}; +use crate::Zero; +use cssparser::Parser; +use style_traits::{ParseError, StyleParseErrorKind}; + +/// A specified alias for FillRule. +pub use crate::values::generics::basic_shape::FillRule; + +/// A specified `clip-path` value. +pub type ClipPath = generic::GenericClipPath<BasicShape, SpecifiedUrl>; + +/// A specified `shape-outside` value. +pub type ShapeOutside = generic::GenericShapeOutside<BasicShape, Image>; + +/// A specified basic shape. +pub type BasicShape = generic::GenericBasicShape< + HorizontalPosition, + VerticalPosition, + LengthPercentage, + NonNegativeLengthPercentage, +>; + +/// The specified value of `inset()` +pub type InsetRect = generic::InsetRect<LengthPercentage, NonNegativeLengthPercentage>; + +/// A specified circle. +pub type Circle = + generic::Circle<HorizontalPosition, VerticalPosition, NonNegativeLengthPercentage>; + +/// A specified ellipse. +pub type Ellipse = + generic::Ellipse<HorizontalPosition, VerticalPosition, NonNegativeLengthPercentage>; + +/// The specified value of `ShapeRadius` +pub type ShapeRadius = generic::ShapeRadius<NonNegativeLengthPercentage>; + +/// The specified value of `Polygon` +pub type Polygon = generic::GenericPolygon<LengthPercentage>; + +#[cfg(feature = "gecko")] +fn is_clip_path_path_enabled(context: &ParserContext) -> bool { + context.chrome_rules_enabled() || static_prefs::pref!("layout.css.clip-path-path.enabled") +} +#[cfg(feature = "servo")] +fn is_clip_path_path_enabled(_: &ParserContext) -> bool { + false +} + +/// A helper for both clip-path and shape-outside parsing of shapes. +fn parse_shape_or_box<'i, 't, R, ReferenceBox>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + to_shape: impl FnOnce(Box<BasicShape>, ReferenceBox) -> R, + to_reference_box: impl FnOnce(ReferenceBox) -> R, +) -> Result<R, ParseError<'i>> +where + ReferenceBox: Default + Parse, +{ + fn parse_component<U: Parse>( + context: &ParserContext, + input: &mut Parser, + component: &mut Option<U>, + ) -> bool { + if component.is_some() { + return false; // already parsed this component + } + + *component = input.try_parse(|i| U::parse(context, i)).ok(); + component.is_some() + } + + let mut shape = None; + let mut ref_box = None; + + while parse_component(context, input, &mut shape) || + parse_component(context, input, &mut ref_box) + { + // + } + + if let Some(shp) = shape { + return Ok(to_shape(Box::new(shp), ref_box.unwrap_or_default())); + } + + match ref_box { + Some(r) => Ok(to_reference_box(r)), + None => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } +} + +impl Parse for ClipPath { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(ClipPath::None); + } + + if is_clip_path_path_enabled(context) { + if let Ok(p) = input.try_parse(|i| Path::parse(context, i)) { + return Ok(ClipPath::Path(p)); + } + } + + if let Ok(url) = input.try_parse(|i| SpecifiedUrl::parse(context, i)) { + return Ok(ClipPath::Url(url)); + } + + parse_shape_or_box(context, input, ClipPath::Shape, ClipPath::Box) + } +} + +impl Parse for ShapeOutside { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // Need to parse this here so that `Image::parse_with_cors_anonymous` + // doesn't parse it. + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(ShapeOutside::None); + } + + if let Ok(image) = input.try_parse(|i| Image::parse_with_cors_anonymous(context, i)) { + debug_assert_ne!(image, Image::None); + return Ok(ShapeOutside::Image(image)); + } + + parse_shape_or_box(context, input, ShapeOutside::Shape, ShapeOutside::Box) + } +} + +impl Parse for BasicShape { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let function = input.expect_function()?.clone(); + input.parse_nested_block(move |i| { + (match_ignore_ascii_case! { &function, + "inset" => return InsetRect::parse_function_arguments(context, i).map(generic::BasicShape::Inset), + "circle" => return Circle::parse_function_arguments(context, i).map(generic::BasicShape::Circle), + "ellipse" => return Ellipse::parse_function_arguments(context, i).map(generic::BasicShape::Ellipse), + "polygon" => return Polygon::parse_function_arguments(context, i).map(generic::BasicShape::Polygon), + _ => Err(()) + }).map_err(|()| { + location.new_custom_error(StyleParseErrorKind::UnexpectedFunction(function.clone())) + }) + }) + } +} + +impl Parse for InsetRect { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("inset")?; + input.parse_nested_block(|i| Self::parse_function_arguments(context, i)) + } +} + +impl InsetRect { + /// Parse the inner function arguments of `inset()` + fn parse_function_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let rect = Rect::parse_with(context, input, LengthPercentage::parse)?; + let round = if input + .try_parse(|i| i.expect_ident_matching("round")) + .is_ok() + { + BorderRadius::parse(context, input)? + } else { + BorderRadius::zero() + }; + Ok(generic::InsetRect { rect, round }) + } +} + +impl Parse for Circle { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("circle")?; + input.parse_nested_block(|i| Self::parse_function_arguments(context, i)) + } +} + +impl Circle { + fn parse_function_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let radius = input + .try_parse(|i| ShapeRadius::parse(context, i)) + .unwrap_or_default(); + let position = if input.try_parse(|i| i.expect_ident_matching("at")).is_ok() { + Position::parse(context, input)? + } else { + Position::center() + }; + + Ok(generic::Circle { radius, position }) + } +} + +impl Parse for Ellipse { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("ellipse")?; + input.parse_nested_block(|i| Self::parse_function_arguments(context, i)) + } +} + +impl Ellipse { + fn parse_function_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let (a, b) = input + .try_parse(|i| -> Result<_, ParseError> { + Ok(( + ShapeRadius::parse(context, i)?, + ShapeRadius::parse(context, i)?, + )) + }) + .unwrap_or_default(); + let position = if input.try_parse(|i| i.expect_ident_matching("at")).is_ok() { + Position::parse(context, input)? + } else { + Position::center() + }; + + Ok(generic::Ellipse { + semiaxis_x: a, + semiaxis_y: b, + position: position, + }) + } +} + +impl Parse for Polygon { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("polygon")?; + input.parse_nested_block(|i| Self::parse_function_arguments(context, i)) + } +} + +impl Polygon { + /// Parse the inner arguments of a `polygon` function. + fn parse_function_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let fill = input + .try_parse(|i| -> Result<_, ParseError> { + let fill = FillRule::parse(i)?; + i.expect_comma()?; // only eat the comma if there is something before it + Ok(fill) + }) + .unwrap_or_default(); + + let coordinates = input + .parse_comma_separated(|i| { + Ok(PolygonCoord( + LengthPercentage::parse(context, i)?, + LengthPercentage::parse(context, i)?, + )) + })? + .into(); + + Ok(Polygon { fill, coordinates }) + } +} + +impl Parse for Path { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("path")?; + input.parse_nested_block(|i| Self::parse_function_arguments(context, i)) + } +} + +impl Path { + /// Parse the inner arguments of a `path` function. + fn parse_function_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let fill = input + .try_parse(|i| -> Result<_, ParseError> { + let fill = FillRule::parse(i)?; + i.expect_comma()?; + Ok(fill) + }) + .unwrap_or_default(); + let path = SVGPathData::parse(context, input)?; + Ok(Path { fill, path }) + } +} diff --git a/servo/components/style/values/specified/border.rs b/servo/components/style/values/specified/border.rs new file mode 100644 index 0000000000..817a5aaddd --- /dev/null +++ b/servo/components/style/values/specified/border.rs @@ -0,0 +1,311 @@ +/* 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/. */ + +//! Specified types for CSS values related to borders. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::{self, Context, ToComputedValue}; +use crate::values::generics::border::BorderCornerRadius as GenericBorderCornerRadius; +use crate::values::generics::border::BorderImageSideWidth as GenericBorderImageSideWidth; +use crate::values::generics::border::BorderImageSlice as GenericBorderImageSlice; +use crate::values::generics::border::BorderRadius as GenericBorderRadius; +use crate::values::generics::border::BorderSpacing as GenericBorderSpacing; +use crate::values::generics::rect::Rect; +use crate::values::generics::size::Size2D; +use crate::values::specified::length::{NonNegativeLength, NonNegativeLengthPercentage}; +use crate::values::specified::{AllowQuirks, NonNegativeNumber, NonNegativeNumberOrPercentage}; +use crate::Zero; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, ToCss}; + +/// A specified value for a single side of a `border-style` property. +/// +/// The order here corresponds to the integer values from the border conflict +/// resolution rules in CSS 2.1 § 17.6.2.1. Higher values override lower values. +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + MallocSizeOf, + Ord, + Parse, + PartialEq, + PartialOrd, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum BorderStyle { + Hidden, + None, + Inset, + Groove, + Outset, + Ridge, + Dotted, + Dashed, + Solid, + Double, +} + +impl BorderStyle { + /// Whether this border style is either none or hidden. + #[inline] + pub fn none_or_hidden(&self) -> bool { + matches!(*self, BorderStyle::None | BorderStyle::Hidden) + } +} + +/// A specified value for a single side of the `border-width` property. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum BorderSideWidth { + /// `thin` + Thin, + /// `medium` + Medium, + /// `thick` + Thick, + /// `<length>` + Length(NonNegativeLength), +} + +/// A specified value for the `border-image-width` property. +pub type BorderImageWidth = Rect<BorderImageSideWidth>; + +/// A specified value for a single side of a `border-image-width` property. +pub type BorderImageSideWidth = + GenericBorderImageSideWidth<NonNegativeLengthPercentage, NonNegativeNumber>; + +/// A specified value for the `border-image-slice` property. +pub type BorderImageSlice = GenericBorderImageSlice<NonNegativeNumberOrPercentage>; + +/// A specified value for the `border-radius` property. +pub type BorderRadius = GenericBorderRadius<NonNegativeLengthPercentage>; + +/// A specified value for the `border-*-radius` longhand properties. +pub type BorderCornerRadius = GenericBorderCornerRadius<NonNegativeLengthPercentage>; + +/// A specified value for the `border-spacing` longhand properties. +pub type BorderSpacing = GenericBorderSpacing<NonNegativeLength>; + +impl BorderImageSlice { + /// Returns the `100%` value. + #[inline] + pub fn hundred_percent() -> Self { + GenericBorderImageSlice { + offsets: Rect::all(NonNegativeNumberOrPercentage::hundred_percent()), + fill: false, + } + } +} + +impl BorderSideWidth { + /// Returns the `0px` value. + #[inline] + pub fn zero() -> Self { + BorderSideWidth::Length(NonNegativeLength::zero()) + } + + /// Parses, with quirks. + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + if let Ok(length) = + input.try_parse(|i| NonNegativeLength::parse_quirky(context, i, allow_quirks)) + { + return Ok(BorderSideWidth::Length(length)); + } + try_match_ident_ignore_ascii_case! { input, + "thin" => Ok(BorderSideWidth::Thin), + "medium" => Ok(BorderSideWidth::Medium), + "thick" => Ok(BorderSideWidth::Thick), + } + } +} + +impl Parse for BorderSideWidth { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_quirky(context, input, AllowQuirks::No) + } +} + +impl ToComputedValue for BorderSideWidth { + type ComputedValue = computed::NonNegativeLength; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + // We choose the pixel length of the keyword values the same as both spec and gecko. + // Spec: https://drafts.csswg.org/css-backgrounds-3/#line-width + // Gecko: https://bugzilla.mozilla.org/show_bug.cgi?id=1312155#c0 + match *self { + BorderSideWidth::Thin => NonNegativeLength::from_px(1.).to_computed_value(context), + BorderSideWidth::Medium => NonNegativeLength::from_px(3.).to_computed_value(context), + BorderSideWidth::Thick => NonNegativeLength::from_px(5.).to_computed_value(context), + BorderSideWidth::Length(ref length) => length.to_computed_value(context), + } + .into() + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + BorderSideWidth::Length(ToComputedValue::from_computed_value(computed)) + } +} + +impl BorderImageSideWidth { + /// Returns `1`. + #[inline] + pub fn one() -> Self { + GenericBorderImageSideWidth::Number(NonNegativeNumber::new(1.)) + } +} + +impl Parse for BorderImageSlice { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut fill = input.try_parse(|i| i.expect_ident_matching("fill")).is_ok(); + let offsets = Rect::parse_with(context, input, NonNegativeNumberOrPercentage::parse)?; + if !fill { + fill = input.try_parse(|i| i.expect_ident_matching("fill")).is_ok(); + } + Ok(GenericBorderImageSlice { offsets, fill }) + } +} + +impl Parse for BorderRadius { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let widths = Rect::parse_with(context, input, NonNegativeLengthPercentage::parse)?; + let heights = if input.try_parse(|i| i.expect_delim('/')).is_ok() { + Rect::parse_with(context, input, NonNegativeLengthPercentage::parse)? + } else { + widths.clone() + }; + + Ok(GenericBorderRadius { + top_left: BorderCornerRadius::new(widths.0, heights.0), + top_right: BorderCornerRadius::new(widths.1, heights.1), + bottom_right: BorderCornerRadius::new(widths.2, heights.2), + bottom_left: BorderCornerRadius::new(widths.3, heights.3), + }) + } +} + +impl Parse for BorderCornerRadius { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Size2D::parse_with(context, input, NonNegativeLengthPercentage::parse) + .map(GenericBorderCornerRadius) + } +} + +impl Parse for BorderSpacing { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Size2D::parse_with(context, input, |context, input| { + NonNegativeLength::parse_quirky(context, input, AllowQuirks::Yes) + }) + .map(GenericBorderSpacing) + } +} + +/// A single border-image-repeat keyword. +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub enum BorderImageRepeatKeyword { + Stretch, + Repeat, + Round, + Space, +} + +/// The specified value for the `border-image-repeat` property. +/// +/// https://drafts.csswg.org/css-backgrounds/#the-border-image-repeat +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +pub struct BorderImageRepeat(pub BorderImageRepeatKeyword, pub BorderImageRepeatKeyword); + +impl ToCss for BorderImageRepeat { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.0.to_css(dest)?; + if self.0 != self.1 { + dest.write_str(" ")?; + self.1.to_css(dest)?; + } + Ok(()) + } +} + +impl BorderImageRepeat { + /// Returns the `stretch` value. + #[inline] + pub fn stretch() -> Self { + BorderImageRepeat( + BorderImageRepeatKeyword::Stretch, + BorderImageRepeatKeyword::Stretch, + ) + } +} + +impl Parse for BorderImageRepeat { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let horizontal = BorderImageRepeatKeyword::parse(input)?; + let vertical = input.try_parse(BorderImageRepeatKeyword::parse).ok(); + Ok(BorderImageRepeat( + horizontal, + vertical.unwrap_or(horizontal), + )) + } +} diff --git a/servo/components/style/values/specified/box.rs b/servo/components/style/values/specified/box.rs new file mode 100644 index 0000000000..6511418870 --- /dev/null +++ b/servo/components/style/values/specified/box.rs @@ -0,0 +1,1996 @@ +/* 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/. */ + +//! Specified types for box properties. + +use crate::custom_properties::Name as CustomPropertyName; +use crate::parser::{Parse, ParserContext}; +use crate::properties::{LonghandId, PropertyDeclarationId, PropertyFlags}; +use crate::properties::{PropertyId, ShorthandId}; +use crate::values::generics::box_::AnimationIterationCount as GenericAnimationIterationCount; +use crate::values::generics::box_::Perspective as GenericPerspective; +use crate::values::generics::box_::{GenericVerticalAlign, VerticalAlignKeyword}; +use crate::values::specified::length::{LengthPercentage, NonNegativeLength}; +use crate::values::specified::{AllowQuirks, Number}; +use crate::values::{CustomIdent, KeyframesName}; +use crate::Atom; +use cssparser::Parser; +use num_traits::FromPrimitive; +use selectors::parser::SelectorParseErrorKind; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, KeywordsCollectFn, ParseError}; +use style_traits::{SpecifiedValueInfo, StyleParseErrorKind, ToCss}; + +#[cfg(feature = "gecko")] +fn moz_display_values_enabled(context: &ParserContext) -> bool { + context.in_ua_or_chrome_sheet() || + static_prefs::pref!("layout.css.xul-display-values.content.enabled") +} + +#[cfg(feature = "gecko")] +fn moz_box_display_values_enabled(context: &ParserContext) -> bool { + context.in_ua_or_chrome_sheet() || + static_prefs::pref!("layout.css.xul-box-display-values.content.enabled") +} + +fn flexbox_enabled() -> bool { + #[cfg(feature = "servo-layout-2020")] + { + return servo_config::prefs::pref_map() + .get("layout.flexbox.enabled") + .as_bool() + .unwrap_or(false); + } + + true +} + +/// Defines an element’s display type, which consists of +/// the two basic qualities of how an element generates boxes +/// <https://drafts.csswg.org/css-display/#propdef-display> +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, Eq, FromPrimitive, Hash, MallocSizeOf, PartialEq, ToCss, ToShmem)] +#[repr(u8)] +pub enum DisplayOutside { + None = 0, + Inline, + Block, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableCaption, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + InternalTable, + #[cfg(feature = "gecko")] + InternalRuby, + #[cfg(feature = "gecko")] + XUL, +} + +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, Eq, FromPrimitive, Hash, MallocSizeOf, PartialEq, ToCss, ToShmem)] +#[repr(u8)] +pub enum DisplayInside { + None = 0, + #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))] + Contents, + Flow, + FlowRoot, + Flex, + #[cfg(feature = "gecko")] + Grid, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + Table, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableRowGroup, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableColumn, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableColumnGroup, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableHeaderGroup, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableFooterGroup, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableRow, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableCell, + #[cfg(feature = "gecko")] + Ruby, + #[cfg(feature = "gecko")] + RubyBase, + #[cfg(feature = "gecko")] + RubyBaseContainer, + #[cfg(feature = "gecko")] + RubyText, + #[cfg(feature = "gecko")] + RubyTextContainer, + #[cfg(feature = "gecko")] + WebkitBox, + #[cfg(feature = "gecko")] + MozBox, + #[cfg(feature = "gecko")] + MozStack, + #[cfg(feature = "gecko")] + MozDeck, + #[cfg(feature = "gecko")] + MozPopup, +} + +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + Hash, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct Display(u16); + +/// Gecko-only impl block for Display (shared stuff later in this file): +#[allow(missing_docs)] +#[allow(non_upper_case_globals)] +impl Display { + // Our u16 bits are used as follows: LOOOOOOOIIIIIIII + const LIST_ITEM_BIT: u16 = 0x8000; //^ + const DISPLAY_OUTSIDE_BITS: u16 = 7; // ^^^^^^^ + const DISPLAY_INSIDE_BITS: u16 = 8; // ^^^^^^^^ + + /// https://drafts.csswg.org/css-display/#the-display-properties + pub const None: Self = Self::new(DisplayOutside::None, DisplayInside::None); + #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))] + pub const Contents: Self = Self::new(DisplayOutside::None, DisplayInside::Contents); + pub const Inline: Self = Self::new(DisplayOutside::Inline, DisplayInside::Flow); + pub const InlineBlock: Self = Self::new(DisplayOutside::Inline, DisplayInside::FlowRoot); + pub const Block: Self = Self::new(DisplayOutside::Block, DisplayInside::Flow); + #[cfg(feature = "gecko")] + pub const FlowRoot: Self = Self::new(DisplayOutside::Block, DisplayInside::FlowRoot); + pub const Flex: Self = Self::new(DisplayOutside::Block, DisplayInside::Flex); + pub const InlineFlex: Self = Self::new(DisplayOutside::Inline, DisplayInside::Flex); + #[cfg(feature = "gecko")] + pub const Grid: Self = Self::new(DisplayOutside::Block, DisplayInside::Grid); + #[cfg(feature = "gecko")] + pub const InlineGrid: Self = Self::new(DisplayOutside::Inline, DisplayInside::Grid); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const Table: Self = Self::new(DisplayOutside::Block, DisplayInside::Table); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const InlineTable: Self = Self::new(DisplayOutside::Inline, DisplayInside::Table); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableCaption: Self = Self::new(DisplayOutside::TableCaption, DisplayInside::Flow); + #[cfg(feature = "gecko")] + pub const Ruby: Self = Self::new(DisplayOutside::Inline, DisplayInside::Ruby); + #[cfg(feature = "gecko")] + pub const WebkitBox: Self = Self::new(DisplayOutside::Block, DisplayInside::WebkitBox); + #[cfg(feature = "gecko")] + pub const WebkitInlineBox: Self = Self::new(DisplayOutside::Inline, DisplayInside::WebkitBox); + + // Internal table boxes. + + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableRowGroup: Self = + Self::new(DisplayOutside::InternalTable, DisplayInside::TableRowGroup); + + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableHeaderGroup: Self = Self::new( + DisplayOutside::InternalTable, + DisplayInside::TableHeaderGroup, + ); + + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableFooterGroup: Self = Self::new( + DisplayOutside::InternalTable, + DisplayInside::TableFooterGroup, + ); + + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableColumn: Self = + Self::new(DisplayOutside::InternalTable, DisplayInside::TableColumn); + + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableColumnGroup: Self = Self::new( + DisplayOutside::InternalTable, + DisplayInside::TableColumnGroup, + ); + + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableRow: Self = Self::new(DisplayOutside::InternalTable, DisplayInside::TableRow); + + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableCell: Self = Self::new(DisplayOutside::InternalTable, DisplayInside::TableCell); + + /// Internal ruby boxes. + #[cfg(feature = "gecko")] + pub const RubyBase: Self = Self::new(DisplayOutside::InternalRuby, DisplayInside::RubyBase); + #[cfg(feature = "gecko")] + pub const RubyBaseContainer: Self = Self::new( + DisplayOutside::InternalRuby, + DisplayInside::RubyBaseContainer, + ); + #[cfg(feature = "gecko")] + pub const RubyText: Self = Self::new(DisplayOutside::InternalRuby, DisplayInside::RubyText); + #[cfg(feature = "gecko")] + pub const RubyTextContainer: Self = Self::new( + DisplayOutside::InternalRuby, + DisplayInside::RubyTextContainer, + ); + + /// XUL boxes. + #[cfg(feature = "gecko")] + pub const MozBox: Self = Self::new(DisplayOutside::Block, DisplayInside::MozBox); + #[cfg(feature = "gecko")] + pub const MozInlineBox: Self = Self::new(DisplayOutside::Inline, DisplayInside::MozBox); + #[cfg(feature = "gecko")] + pub const MozStack: Self = Self::new(DisplayOutside::XUL, DisplayInside::MozStack); + #[cfg(feature = "gecko")] + pub const MozDeck: Self = Self::new(DisplayOutside::XUL, DisplayInside::MozDeck); + #[cfg(feature = "gecko")] + pub const MozPopup: Self = Self::new(DisplayOutside::XUL, DisplayInside::MozPopup); + + /// Make a raw display value from <display-outside> and <display-inside> values. + #[inline] + const fn new(outside: DisplayOutside, inside: DisplayInside) -> Self { + let o: u16 = ((outside as u8) as u16) << Self::DISPLAY_INSIDE_BITS; + let i: u16 = (inside as u8) as u16; + Self(o | i) + } + + /// Make a display enum value from <display-outside> and <display-inside> values. + #[inline] + fn from3(outside: DisplayOutside, inside: DisplayInside, list_item: bool) -> Self { + let v = Self::new(outside, inside); + if !list_item { + return v; + } + Self(v.0 | Self::LIST_ITEM_BIT) + } + + /// Accessor for the <display-inside> value. + #[inline] + pub fn inside(&self) -> DisplayInside { + DisplayInside::from_u16(self.0 & ((1 << Self::DISPLAY_INSIDE_BITS) - 1)).unwrap() + } + + /// Accessor for the <display-outside> value. + #[inline] + pub fn outside(&self) -> DisplayOutside { + DisplayOutside::from_u16( + (self.0 >> Self::DISPLAY_INSIDE_BITS) & ((1 << Self::DISPLAY_OUTSIDE_BITS) - 1), + ) + .unwrap() + } + + /// Whether this is `display: inline` (or `inline list-item`). + #[inline] + pub fn is_inline_flow(&self) -> bool { + self.outside() == DisplayOutside::Inline && self.inside() == DisplayInside::Flow + } + + /// Returns whether this `display` value is some kind of list-item. + #[inline] + pub const fn is_list_item(&self) -> bool { + (self.0 & Self::LIST_ITEM_BIT) != 0 + } + + /// Returns whether this `display` value is a ruby level container. + pub fn is_ruby_level_container(&self) -> bool { + match *self { + #[cfg(feature = "gecko")] + Display::RubyBaseContainer | Display::RubyTextContainer => true, + _ => false, + } + } + + /// Returns whether this `display` value is one of the types for ruby. + pub fn is_ruby_type(&self) -> bool { + match self.inside() { + #[cfg(feature = "gecko")] + DisplayInside::Ruby | + DisplayInside::RubyBase | + DisplayInside::RubyText | + DisplayInside::RubyBaseContainer | + DisplayInside::RubyTextContainer => true, + _ => false, + } + } +} + +/// Shared Display impl for both Gecko and Servo. +#[allow(non_upper_case_globals)] +impl Display { + /// The initial display value. + #[inline] + pub fn inline() -> Self { + Display::Inline + } + + /// <https://drafts.csswg.org/css2/visuren.html#x13> + #[cfg(feature = "servo")] + #[inline] + pub fn is_atomic_inline_level(&self) -> bool { + match *self { + Display::InlineBlock | Display::InlineFlex => true, + #[cfg(any(feature = "servo-layout-2013"))] + Display::InlineTable => true, + _ => false, + } + } + + /// Returns whether this `display` value is the display of a flex or + /// grid container. + /// + /// This is used to implement various style fixups. + pub fn is_item_container(&self) -> bool { + match self.inside() { + DisplayInside::Flex => true, + #[cfg(feature = "gecko")] + DisplayInside::Grid => true, + _ => false, + } + } + + /// Returns whether an element with this display type is a line + /// participant, which means it may lay its children on the same + /// line as itself. + pub fn is_line_participant(&self) -> bool { + match *self { + Display::Inline => true, + #[cfg(feature = "gecko")] + Display::Contents | Display::Ruby | Display::RubyBaseContainer => true, + _ => false, + } + } + + /// Convert this display into an equivalent block display. + /// + /// Also used for :root style adjustments. + pub fn equivalent_block_display(&self, _is_root_element: bool) -> Self { + #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))] + { + // Special handling for `contents` and `list-item`s on the root element. + if _is_root_element && (self.is_contents() || self.is_list_item()) { + return Display::Block; + } + } + + match self.outside() { + DisplayOutside::Inline => { + let inside = match self.inside() { + // `inline-block` blockifies to `block` rather than + // `flow-root`, for legacy reasons. + DisplayInside::FlowRoot => DisplayInside::Flow, + inside => inside, + }; + Display::from3(DisplayOutside::Block, inside, self.is_list_item()) + }, + DisplayOutside::Block | DisplayOutside::None => *self, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + _ => Display::Block, + } + } + + /// Convert this display into an equivalent inline-outside display. + /// https://drafts.csswg.org/css-display/#inlinify + #[cfg(feature = "gecko")] + pub fn inlinify(&self) -> Self { + match self.outside() { + DisplayOutside::Block => { + let inside = match self.inside() { + // `display: block` inlinifies to `display: inline-block`, + // rather than `inline`, for legacy reasons. + DisplayInside::Flow => DisplayInside::FlowRoot, + inside => inside, + }; + Display::from3(DisplayOutside::Inline, inside, self.is_list_item()) + }, + _ => *self, + } + } + + /// Returns true if the value is `Contents` + #[inline] + pub fn is_contents(&self) -> bool { + match *self { + #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))] + Display::Contents => true, + _ => false, + } + } + + /// Returns true if the value is `None` + #[inline] + pub fn is_none(&self) -> bool { + *self == Display::None + } +} + +impl ToCss for Display { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + let outside = self.outside(); + let inside = self.inside(); + match *self { + Display::Block | Display::Inline => outside.to_css(dest), + Display::InlineBlock => dest.write_str("inline-block"), + #[cfg(feature = "gecko")] + Display::WebkitInlineBox => dest.write_str("-webkit-inline-box"), + #[cfg(feature = "gecko")] + Display::MozInlineBox => dest.write_str("-moz-inline-box"), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + Display::TableCaption => dest.write_str("table-caption"), + _ => match (outside, inside) { + #[cfg(feature = "gecko")] + (DisplayOutside::Inline, DisplayInside::Grid) => dest.write_str("inline-grid"), + (DisplayOutside::Inline, DisplayInside::Flex) => dest.write_str("inline-flex"), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + (DisplayOutside::Inline, DisplayInside::Table) => dest.write_str("inline-table"), + #[cfg(feature = "gecko")] + (DisplayOutside::Block, DisplayInside::Ruby) => dest.write_str("block ruby"), + (_, inside) => { + if self.is_list_item() { + if outside != DisplayOutside::Block { + outside.to_css(dest)?; + dest.write_str(" ")?; + } + if inside != DisplayInside::Flow { + inside.to_css(dest)?; + dest.write_str(" ")?; + } + dest.write_str("list-item") + } else { + inside.to_css(dest) + } + }, + }, + } + } +} + +/// <display-inside> = flow | flow-root | table | flex | grid | ruby +/// https://drafts.csswg.org/css-display/#typedef-display-inside +fn parse_display_inside<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<DisplayInside, ParseError<'i>> { + Ok(try_match_ident_ignore_ascii_case! { input, + "flow" => DisplayInside::Flow, + "flex" if flexbox_enabled() => DisplayInside::Flex, + #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))] + "flow-root" => DisplayInside::FlowRoot, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table" => DisplayInside::Table, + #[cfg(feature = "gecko")] + "grid" => DisplayInside::Grid, + #[cfg(feature = "gecko")] + "ruby" => DisplayInside::Ruby, + }) +} + +/// <display-outside> = block | inline | run-in +/// https://drafts.csswg.org/css-display/#typedef-display-outside +fn parse_display_outside<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<DisplayOutside, ParseError<'i>> { + Ok(try_match_ident_ignore_ascii_case! { input, + "block" => DisplayOutside::Block, + "inline" => DisplayOutside::Inline, + // FIXME(bug 2056): not supported in layout yet: + //"run-in" => DisplayOutside::RunIn, + }) +} + +/// (flow | flow-root)? +fn parse_display_inside_for_list_item<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<DisplayInside, ParseError<'i>> { + Ok(try_match_ident_ignore_ascii_case! { input, + "flow" => DisplayInside::Flow, + #[cfg(feature = "gecko")] + "flow-root" => DisplayInside::FlowRoot, + }) +} +/// Test a <display-inside> Result for same values as above. +fn is_valid_inside_for_list_item<'i>(inside: &Result<DisplayInside, ParseError<'i>>) -> bool { + match inside { + Ok(DisplayInside::Flow) => true, + #[cfg(feature = "gecko")] + Ok(DisplayInside::FlowRoot) => true, + _ => false, + } +} + +/// Parse `list-item`. +fn parse_list_item<'i, 't>(input: &mut Parser<'i, 't>) -> Result<(), ParseError<'i>> { + Ok(try_match_ident_ignore_ascii_case! { input, + "list-item" => (), + }) +} + +impl Parse for Display { + #[allow(unused)] // `context` isn't used for servo-2020 for now + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Display, ParseError<'i>> { + // Parse all combinations of <display-inside/outside>? and `list-item`? first. + let mut got_list_item = input.try_parse(parse_list_item).is_ok(); + let mut inside = if got_list_item { + input.try_parse(parse_display_inside_for_list_item) + } else { + input.try_parse(parse_display_inside) + }; + // <display-listitem> = <display-outside>? && [ flow | flow-root ]? && list-item + // https://drafts.csswg.org/css-display/#typedef-display-listitem + if !got_list_item && is_valid_inside_for_list_item(&inside) { + got_list_item = input.try_parse(parse_list_item).is_ok(); + } + let outside = input.try_parse(parse_display_outside); + if outside.is_ok() { + if !got_list_item && (inside.is_err() || is_valid_inside_for_list_item(&inside)) { + got_list_item = input.try_parse(parse_list_item).is_ok(); + } + if inside.is_err() { + inside = if got_list_item { + input.try_parse(parse_display_inside_for_list_item) + } else { + input.try_parse(parse_display_inside) + }; + if !got_list_item && is_valid_inside_for_list_item(&inside) { + got_list_item = input.try_parse(parse_list_item).is_ok(); + } + } + } + if got_list_item || inside.is_ok() || outside.is_ok() { + let inside = inside.unwrap_or(DisplayInside::Flow); + let outside = outside.unwrap_or(match inside { + // "If <display-outside> is omitted, the element’s outside display type + // defaults to block — except for ruby, which defaults to inline." + // https://drafts.csswg.org/css-display/#inside-model + #[cfg(feature = "gecko")] + DisplayInside::Ruby => DisplayOutside::Inline, + _ => DisplayOutside::Block, + }); + return Ok(Display::from3(outside, inside, got_list_item)); + } + + // Now parse the single-keyword `display` values. + Ok(try_match_ident_ignore_ascii_case! { input, + "none" => Display::None, + #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))] + "contents" => Display::Contents, + "inline-block" => Display::InlineBlock, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "inline-table" => Display::InlineTable, + "-webkit-flex" if flexbox_enabled() => Display::Flex, + "inline-flex" | "-webkit-inline-flex" if flexbox_enabled() => Display::InlineFlex, + #[cfg(feature = "gecko")] + "inline-grid" => Display::InlineGrid, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-caption" => Display::TableCaption, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-row-group" => Display::TableRowGroup, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-header-group" => Display::TableHeaderGroup, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-footer-group" => Display::TableFooterGroup, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-column" => Display::TableColumn, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-column-group" => Display::TableColumnGroup, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-row" => Display::TableRow, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-cell" => Display::TableCell, + #[cfg(feature = "gecko")] + "ruby-base" => Display::RubyBase, + #[cfg(feature = "gecko")] + "ruby-base-container" => Display::RubyBaseContainer, + #[cfg(feature = "gecko")] + "ruby-text" => Display::RubyText, + #[cfg(feature = "gecko")] + "ruby-text-container" => Display::RubyTextContainer, + #[cfg(feature = "gecko")] + "-webkit-box" => Display::WebkitBox, + #[cfg(feature = "gecko")] + "-webkit-inline-box" => Display::WebkitInlineBox, + #[cfg(feature = "gecko")] + "-moz-box" if moz_box_display_values_enabled(context) => Display::MozBox, + #[cfg(feature = "gecko")] + "-moz-inline-box" if moz_box_display_values_enabled(context) => Display::MozInlineBox, + #[cfg(feature = "gecko")] + "-moz-stack" if moz_display_values_enabled(context) => Display::MozStack, + #[cfg(feature = "gecko")] + "-moz-deck" if moz_display_values_enabled(context) => Display::MozDeck, + #[cfg(feature = "gecko")] + "-moz-popup" if moz_display_values_enabled(context) => Display::MozPopup, + }) + } +} + +impl SpecifiedValueInfo for Display { + fn collect_completion_keywords(f: KeywordsCollectFn) { + f(&[ + "block", + "contents", + "flex", + "flow-root", + "flow-root list-item", + "grid", + "inline", + "inline-block", + "inline-flex", + "inline-grid", + "inline-table", + "inline list-item", + "inline flow-root list-item", + "list-item", + "none", + "block ruby", + "ruby", + "ruby-base", + "ruby-base-container", + "ruby-text", + "ruby-text-container", + "table", + "table-caption", + "table-cell", + "table-column", + "table-column-group", + "table-footer-group", + "table-header-group", + "table-row", + "table-row-group", + "-webkit-box", + "-webkit-inline-box", + ]); + } +} + +/// A specified value for the `vertical-align` property. +pub type VerticalAlign = GenericVerticalAlign<LengthPercentage>; + +impl Parse for VerticalAlign { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(lp) = + input.try_parse(|i| LengthPercentage::parse_quirky(context, i, AllowQuirks::Yes)) + { + return Ok(GenericVerticalAlign::Length(lp)); + } + + Ok(GenericVerticalAlign::Keyword(VerticalAlignKeyword::parse( + input, + )?)) + } +} + +/// https://drafts.csswg.org/css-animations/#animation-iteration-count +pub type AnimationIterationCount = GenericAnimationIterationCount<Number>; + +impl Parse for AnimationIterationCount { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut ::cssparser::Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("infinite")) + .is_ok() + { + return Ok(GenericAnimationIterationCount::Infinite); + } + + let number = Number::parse_non_negative(context, input)?; + Ok(GenericAnimationIterationCount::Number(number)) + } +} + +impl AnimationIterationCount { + /// Returns the value `1.0`. + #[inline] + pub fn one() -> Self { + GenericAnimationIterationCount::Number(Number::new(1.0)) + } +} + +/// A value for the `animation-name` property. +#[derive( + Clone, + Debug, + Eq, + Hash, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[value_info(other_values = "none")] +pub struct AnimationName(pub Option<KeyframesName>); + +impl AnimationName { + /// Get the name of the animation as an `Atom`. + pub fn as_atom(&self) -> Option<&Atom> { + self.0.as_ref().map(|n| n.as_atom()) + } + + /// Returns the `none` value. + pub fn none() -> Self { + AnimationName(None) + } +} + +impl ToCss for AnimationName { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match self.0 { + Some(ref name) => name.to_css(dest), + None => dest.write_str("none"), + } + } +} + +impl Parse for AnimationName { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(name) = input.try_parse(|input| KeyframesName::parse(context, input)) { + return Ok(AnimationName(Some(name))); + } + + input.expect_ident_matching("none")?; + Ok(AnimationName(None)) + } +} + +/// https://drafts.csswg.org/css-scroll-snap-1/#snap-axis +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ScrollSnapAxis { + X, + Y, + Block, + Inline, + Both, +} + +/// https://drafts.csswg.org/css-scroll-snap-1/#snap-strictness +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ScrollSnapStrictness { + #[css(skip)] + None, // Used to represent scroll-snap-type: none. It's not parsed. + Mandatory, + Proximity, +} + +/// https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-type +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct ScrollSnapType { + axis: ScrollSnapAxis, + strictness: ScrollSnapStrictness, +} + +impl ScrollSnapType { + /// Returns `none`. + #[inline] + pub fn none() -> Self { + Self { + axis: ScrollSnapAxis::Both, + strictness: ScrollSnapStrictness::None, + } + } +} + +impl Parse for ScrollSnapType { + /// none | [ x | y | block | inline | both ] [ mandatory | proximity ]? + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("none")) + .is_ok() + { + return Ok(ScrollSnapType::none()); + } + + let axis = ScrollSnapAxis::parse(input)?; + let strictness = input + .try_parse(ScrollSnapStrictness::parse) + .unwrap_or(ScrollSnapStrictness::Proximity); + Ok(Self { axis, strictness }) + } +} + +impl ToCss for ScrollSnapType { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.strictness == ScrollSnapStrictness::None { + return dest.write_str("none"); + } + self.axis.to_css(dest)?; + if self.strictness != ScrollSnapStrictness::Proximity { + dest.write_str(" ")?; + self.strictness.to_css(dest)?; + } + Ok(()) + } +} + +/// Specified value of scroll-snap-align keyword value. +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ScrollSnapAlignKeyword { + None, + Start, + End, + Center, +} + +/// https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-align +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct ScrollSnapAlign { + block: ScrollSnapAlignKeyword, + inline: ScrollSnapAlignKeyword, +} + +impl ScrollSnapAlign { + /// Returns `none`. + #[inline] + pub fn none() -> Self { + ScrollSnapAlign { + block: ScrollSnapAlignKeyword::None, + inline: ScrollSnapAlignKeyword::None, + } + } +} + +impl Parse for ScrollSnapAlign { + /// [ none | start | end | center ]{1,2} + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<ScrollSnapAlign, ParseError<'i>> { + let block = ScrollSnapAlignKeyword::parse(input)?; + let inline = input + .try_parse(ScrollSnapAlignKeyword::parse) + .unwrap_or(block); + Ok(ScrollSnapAlign { block, inline }) + } +} + +impl ToCss for ScrollSnapAlign { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.block.to_css(dest)?; + if self.block != self.inline { + dest.write_str(" ")?; + self.inline.to_css(dest)?; + } + Ok(()) + } +} + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum OverscrollBehavior { + Auto, + Contain, + None, +} + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum OverflowAnchor { + Auto, + None, +} + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum OverflowClipBox { + PaddingBox, + ContentBox, +} + +#[derive( + Clone, + Debug, + Default, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[css(comma)] +#[repr(C)] +/// Provides a rendering hint to the user agent, stating what kinds of changes +/// the author expects to perform on the element. +/// +/// `auto` is represented by an empty `features` list. +/// +/// <https://drafts.csswg.org/css-will-change/#will-change> +pub struct WillChange { + /// The features that are supposed to change. + /// + /// TODO(emilio): Consider using ArcSlice since we just clone them from the + /// specified value? That'd save an allocation, which could be worth it. + #[css(iterable, if_empty = "auto")] + features: crate::OwnedSlice<CustomIdent>, + /// A bitfield with the kind of change that the value will create, based + /// on the above field. + #[css(skip)] + bits: WillChangeBits, +} + +impl WillChange { + #[inline] + /// Get default value of `will-change` as `auto` + pub fn auto() -> Self { + Self::default() + } +} + +bitflags! { + /// The change bits that we care about. + #[derive(Default, MallocSizeOf, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem)] + #[repr(C)] + pub struct WillChangeBits: u8 { + /// Whether the stacking context will change. + const STACKING_CONTEXT = 1 << 0; + /// Whether `transform` will change. + const TRANSFORM = 1 << 1; + /// Whether `scroll-position` will change. + const SCROLL = 1 << 2; + /// Whether `opacity` will change. + const OPACITY = 1 << 3; + /// Fixed pos containing block. + const FIXPOS_CB = 1 << 4; + /// Abs pos containing block. + const ABSPOS_CB = 1 << 5; + } +} + +fn change_bits_for_longhand(longhand: LonghandId) -> WillChangeBits { + let mut flags = match longhand { + LonghandId::Opacity => WillChangeBits::OPACITY, + LonghandId::Transform => WillChangeBits::TRANSFORM, + #[cfg(feature = "gecko")] + LonghandId::Translate | LonghandId::Rotate | LonghandId::Scale | LonghandId::OffsetPath => { + WillChangeBits::TRANSFORM + }, + _ => WillChangeBits::empty(), + }; + + let property_flags = longhand.flags(); + if property_flags.contains(PropertyFlags::CREATES_STACKING_CONTEXT) { + flags |= WillChangeBits::STACKING_CONTEXT; + } + if property_flags.contains(PropertyFlags::FIXPOS_CB) { + flags |= WillChangeBits::FIXPOS_CB; + } + if property_flags.contains(PropertyFlags::ABSPOS_CB) { + flags |= WillChangeBits::ABSPOS_CB; + } + flags +} + +fn change_bits_for_maybe_property(ident: &str, context: &ParserContext) -> WillChangeBits { + let id = match PropertyId::parse_ignoring_rule_type(ident, context) { + Ok(id) => id, + Err(..) => return WillChangeBits::empty(), + }; + + match id.as_shorthand() { + Ok(shorthand) => shorthand + .longhands() + .fold(WillChangeBits::empty(), |flags, p| { + flags | change_bits_for_longhand(p) + }), + Err(PropertyDeclarationId::Longhand(longhand)) => change_bits_for_longhand(longhand), + Err(PropertyDeclarationId::Custom(..)) => WillChangeBits::empty(), + } +} + +impl Parse for WillChange { + /// auto | <animateable-feature># + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("auto")) + .is_ok() + { + return Ok(Self::default()); + } + + let mut bits = WillChangeBits::empty(); + let custom_idents = input.parse_comma_separated(|i| { + let location = i.current_source_location(); + let parser_ident = i.expect_ident()?; + let ident = CustomIdent::from_ident( + location, + parser_ident, + &["will-change", "none", "all", "auto"], + )?; + + if ident.0 == atom!("scroll-position") { + bits |= WillChangeBits::SCROLL; + } else { + bits |= change_bits_for_maybe_property(&parser_ident, context); + } + Ok(ident) + })?; + + Ok(Self { + features: custom_idents.into(), + bits, + }) + } +} + +bitflags! { + /// Values for the `touch-action` property. + #[derive(MallocSizeOf, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem)] + /// These constants match Gecko's `NS_STYLE_TOUCH_ACTION_*` constants. + #[value_info(other_values = "auto,none,manipulation,pan-x,pan-y,pinch-zoom")] + #[repr(C)] + pub struct TouchAction: u8 { + /// `none` variant + const NONE = 1 << 0; + /// `auto` variant + const AUTO = 1 << 1; + /// `pan-x` variant + const PAN_X = 1 << 2; + /// `pan-y` variant + const PAN_Y = 1 << 3; + /// `manipulation` variant + const MANIPULATION = 1 << 4; + /// `pinch-zoom` variant + const PINCH_ZOOM = 1 << 5; + } +} + +impl TouchAction { + #[inline] + /// Get default `touch-action` as `auto` + pub fn auto() -> TouchAction { + TouchAction::AUTO + } +} + +impl ToCss for TouchAction { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.contains(TouchAction::AUTO) { + return dest.write_str("auto"); + } + if self.contains(TouchAction::NONE) { + return dest.write_str("none"); + } + if self.contains(TouchAction::MANIPULATION) { + return dest.write_str("manipulation"); + } + + let mut has_any = false; + macro_rules! maybe_write_value { + ($ident:path => $str:expr) => { + if self.contains($ident) { + if has_any { + dest.write_str(" ")?; + } + has_any = true; + dest.write_str($str)?; + } + }; + } + maybe_write_value!(TouchAction::PAN_X => "pan-x"); + maybe_write_value!(TouchAction::PAN_Y => "pan-y"); + maybe_write_value!(TouchAction::PINCH_ZOOM => "pinch-zoom"); + + debug_assert!(has_any); + Ok(()) + } +} + +impl Parse for TouchAction { + /// auto | none | [ pan-x || pan-y || pinch-zoom ] | manipulation + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<TouchAction, ParseError<'i>> { + let mut result = TouchAction::empty(); + while let Ok(name) = input.try_parse(|i| i.expect_ident_cloned()) { + let flag = match_ignore_ascii_case! { &name, + "pan-x" => Some(TouchAction::PAN_X), + "pan-y" => Some(TouchAction::PAN_Y), + "pinch-zoom" => Some(TouchAction::PINCH_ZOOM), + "none" if result.is_empty() => return Ok(TouchAction::NONE), + "manipulation" if result.is_empty() => return Ok(TouchAction::MANIPULATION), + "auto" if result.is_empty() => return Ok(TouchAction::AUTO), + _ => None + }; + + let flag = match flag { + Some(flag) if !result.contains(flag) => flag, + _ => { + return Err( + input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name)) + ); + }, + }; + result.insert(flag); + } + + if !result.is_empty() { + Ok(result) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +bitflags! { + #[derive(MallocSizeOf, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem)] + #[value_info(other_values = "none,strict,content,size,layout,paint")] + #[repr(C)] + /// Constants for contain: https://drafts.csswg.org/css-contain/#contain-property + pub struct Contain: u8 { + /// `none` variant, just for convenience. + const NONE = 0; + /// 'size' variant, turns on size containment + const SIZE = 1 << 0; + /// `layout` variant, turns on layout containment + const LAYOUT = 1 << 1; + /// `paint` variant, turns on paint containment + const PAINT = 1 << 2; + /// `strict` variant, turns on all types of containment + const STRICT = 1 << 3; + /// 'content' variant, turns on layout and paint containment + const CONTENT = 1 << 4; + /// variant with all the bits that contain: strict turns on + const STRICT_BITS = Contain::LAYOUT.bits | Contain::PAINT.bits | Contain::SIZE.bits; + /// variant with all the bits that contain: content turns on + const CONTENT_BITS = Contain::LAYOUT.bits | Contain::PAINT.bits; + } +} + +impl ToCss for Contain { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.is_empty() { + return dest.write_str("none"); + } + if self.contains(Contain::STRICT) { + return dest.write_str("strict"); + } + if self.contains(Contain::CONTENT) { + return dest.write_str("content"); + } + + let mut has_any = false; + macro_rules! maybe_write_value { + ($ident:path => $str:expr) => { + if self.contains($ident) { + if has_any { + dest.write_str(" ")?; + } + has_any = true; + dest.write_str($str)?; + } + }; + } + maybe_write_value!(Contain::SIZE => "size"); + maybe_write_value!(Contain::LAYOUT => "layout"); + maybe_write_value!(Contain::PAINT => "paint"); + + debug_assert!(has_any); + Ok(()) + } +} + +impl Parse for Contain { + /// none | strict | content | [ size || layout || paint ] + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Contain, ParseError<'i>> { + let mut result = Contain::empty(); + while let Ok(name) = input.try_parse(|i| i.expect_ident_cloned()) { + let flag = match_ignore_ascii_case! { &name, + "size" => Some(Contain::SIZE), + "layout" => Some(Contain::LAYOUT), + "paint" => Some(Contain::PAINT), + "strict" if result.is_empty() => return Ok(Contain::STRICT | Contain::STRICT_BITS), + "content" if result.is_empty() => return Ok(Contain::CONTENT | Contain::CONTENT_BITS), + "none" if result.is_empty() => return Ok(result), + _ => None + }; + + let flag = match flag { + Some(flag) if !result.contains(flag) => flag, + _ => { + return Err( + input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name)) + ); + }, + }; + result.insert(flag); + } + + if !result.is_empty() { + Ok(result) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +/// A specified value for the `perspective` property. +pub type Perspective = GenericPerspective<NonNegativeLength>; + +/// A given transition property, that is either `All`, a longhand or shorthand +/// property, or an unsupported or custom property. +#[derive( + Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem, +)] +pub enum TransitionProperty { + /// A shorthand. + Shorthand(ShorthandId), + /// A longhand transitionable property. + Longhand(LonghandId), + /// A custom property. + Custom(CustomPropertyName), + /// Unrecognized property which could be any non-transitionable, custom property, or + /// unknown property. + Unsupported(CustomIdent), +} + +impl ToCss for TransitionProperty { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + use crate::values::serialize_atom_name; + match *self { + TransitionProperty::Shorthand(ref s) => s.to_css(dest), + TransitionProperty::Longhand(ref l) => l.to_css(dest), + TransitionProperty::Custom(ref name) => { + dest.write_str("--")?; + serialize_atom_name(name, dest) + }, + TransitionProperty::Unsupported(ref i) => i.to_css(dest), + } + } +} + +impl Parse for TransitionProperty { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let ident = input.expect_ident()?; + + let id = match PropertyId::parse_ignoring_rule_type(&ident, context) { + Ok(id) => id, + Err(..) => { + return Ok(TransitionProperty::Unsupported(CustomIdent::from_ident( + location, + ident, + &["none"], + )?)); + }, + }; + + Ok(match id.as_shorthand() { + Ok(s) => TransitionProperty::Shorthand(s), + Err(longhand_or_custom) => match longhand_or_custom { + PropertyDeclarationId::Longhand(id) => TransitionProperty::Longhand(id), + PropertyDeclarationId::Custom(custom) => TransitionProperty::Custom(custom.clone()), + }, + }) + } +} + +impl SpecifiedValueInfo for TransitionProperty { + fn collect_completion_keywords(f: KeywordsCollectFn) { + // `transition-property` can actually accept all properties and + // arbitrary identifiers, but `all` is a special one we'd like + // to list. + f(&["all"]); + } +} + +impl TransitionProperty { + /// Returns `all`. + #[inline] + pub fn all() -> Self { + TransitionProperty::Shorthand(ShorthandId::All) + } + + /// Convert TransitionProperty to nsCSSPropertyID. + #[cfg(feature = "gecko")] + pub fn to_nscsspropertyid( + &self, + ) -> Result<crate::gecko_bindings::structs::nsCSSPropertyID, ()> { + Ok(match *self { + TransitionProperty::Shorthand(ShorthandId::All) => { + crate::gecko_bindings::structs::nsCSSPropertyID::eCSSPropertyExtra_all_properties + }, + TransitionProperty::Shorthand(ref id) => id.to_nscsspropertyid(), + TransitionProperty::Longhand(ref id) => id.to_nscsspropertyid(), + TransitionProperty::Custom(..) | TransitionProperty::Unsupported(..) => return Err(()), + }) + } +} + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +/// https://drafts.csswg.org/css-box/#propdef-float +pub enum Float { + Left, + Right, + None, + // https://drafts.csswg.org/css-logical-props/#float-clear + InlineStart, + InlineEnd, +} + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +/// https://drafts.csswg.org/css-box/#propdef-clear +pub enum Clear { + None, + Left, + Right, + Both, + // https://drafts.csswg.org/css-logical-props/#float-clear + InlineStart, + InlineEnd, +} + +/// https://drafts.csswg.org/css-ui/#propdef-resize +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +pub enum Resize { + None, + Both, + Horizontal, + Vertical, + // https://drafts.csswg.org/css-logical-1/#resize + Inline, + Block, +} + +/// The value for the `appearance` property. +/// +/// https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-appearance +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum Appearance { + /// No appearance at all. + None, + /// Default appearance for the element. + /// + /// This value doesn't make sense for -moz-default-appearance, but we don't bother to guard + /// against parsing it. + Auto, + /// A searchfield. + Searchfield, + /// A multi-line text field, e.g. HTML <textarea>. + #[parse(aliases = "textfield-multiline")] + Textarea, + /// A checkbox element. + Checkbox, + /// A radio element within a radio group. + Radio, + /// A dropdown list. + Menulist, + /// List boxes. + Listbox, + /// A horizontal meter bar. + #[parse(aliases = "meterbar")] + Meter, + /// A horizontal progress bar. + #[parse(aliases = "progressbar")] + ProgressBar, + /// A typical dialog button. + Button, + /// A single-line text field, e.g. HTML <input type=text>. + Textfield, + /// The dropdown button(s) that open up a dropdown list. + MenulistButton, + /// Various arrows that go in buttons + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ButtonArrowDown, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ButtonArrowNext, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ButtonArrowPrevious, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ButtonArrowUp, + /// The focus outline box inside of a button. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ButtonFocus, + /// A dual toolbar button (e.g., a Back button with a dropdown) + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Dualbutton, + /// A groupbox. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Groupbox, + /// Menu Bar background + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Menubar, + /// <menu> and <menuitem> appearances + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Menuitem, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Checkmenuitem, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Radiomenuitem, + /// For text on non-iconic menuitems only + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Menuitemtext, + /// The text part of a dropdown list, to left of button. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MenulistText, + /// Menu Popup background. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Menupopup, + /// menu checkbox/radio appearances + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Menucheckbox, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Menuradio, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Menuseparator, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Menuarrow, + /// An image in the menu gutter, like in bookmarks or history. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Menuimage, + /// The meter bar's meter indicator. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Meterchunk, + /// The "arrowed" part of the dropdown button that open up a dropdown list. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozMenulistArrowButton, + /// For HTML's <input type=number> + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + NumberInput, + /// The progress bar's progress indicator + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Progresschunk, + /// A generic container that always repaints on state changes. This is a + /// hack to make XUL checkboxes and radio buttons work. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + CheckboxContainer, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + RadioContainer, + /// The label part of a checkbox or radio button, used for painting a focus + /// outline. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + CheckboxLabel, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + RadioLabel, + /// nsRangeFrame and its subparts + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Range, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + RangeThumb, + /// The resizer background area in a status bar for the resizer widget in + /// the corner of a window. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Resizerpanel, + /// The resizer itself. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Resizer, + /// The scrollbar slider + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ScrollbarHorizontal, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ScrollbarVertical, + /// A scrollbar button (up/down/left/right). + /// Keep these in order (some code casts these values to `int` in order to + /// compare them against each other). + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ScrollbarbuttonUp, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ScrollbarbuttonDown, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ScrollbarbuttonLeft, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ScrollbarbuttonRight, + /// The scrollbar thumb. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ScrollbarthumbHorizontal, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ScrollbarthumbVertical, + /// The scrollbar track. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ScrollbartrackHorizontal, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ScrollbartrackVertical, + /// The scroll corner + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Scrollcorner, + /// A separator. Can be horizontal or vertical. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Separator, + /// A spin control (up/down control for time/date pickers). + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Spinner, + /// The up button of a spin control. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + SpinnerUpbutton, + /// The down button of a spin control. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + SpinnerDownbutton, + /// The textfield of a spin control + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + SpinnerTextfield, + /// A splitter. Can be horizontal or vertical. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Splitter, + /// A status bar in a main application window. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Statusbar, + /// A single pane of a status bar. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Statusbarpanel, + /// A single tab in a tab widget. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Tab, + /// A single pane (inside the tabpanels container). + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Tabpanel, + /// The tab panels container. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Tabpanels, + /// The tabs scroll arrows (left/right). + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + TabScrollArrowBack, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + TabScrollArrowForward, + /// A toolbar in an application window. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Toolbar, + /// A single toolbar button (with no associated dropdown). + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Toolbarbutton, + /// The dropdown portion of a toolbar button + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ToolbarbuttonDropdown, + /// The gripper for a toolbar. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Toolbargripper, + /// The toolbox that contains the toolbars. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Toolbox, + /// A tooltip. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Tooltip, + /// A listbox or tree widget header + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Treeheader, + /// An individual header cell + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Treeheadercell, + /// The sort arrow for a header. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Treeheadersortarrow, + /// A tree item. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Treeitem, + /// A tree widget branch line + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Treeline, + /// A tree widget twisty. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Treetwisty, + /// Open tree widget twisty. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Treetwistyopen, + /// A tree widget. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Treeview, + /// Window and dialog backgrounds. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Window, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Dialog, + + /// Vista Rebars. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWinCommunicationsToolbox, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWinMediaToolbox, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWinBrowsertabbarToolbox, + /// Vista glass. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWinGlass, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWinBorderlessGlass, + /// -moz-apperance style used in setting proper glass margins. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWinExcludeGlass, + + /// Titlebar elements on the Mac. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozMacFullscreenButton, + /// Mac help button. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozMacHelpButton, + + /// Windows themed window frame elements. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWindowButtonBox, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWindowButtonBoxMaximized, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWindowButtonClose, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWindowButtonMaximize, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWindowButtonMinimize, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWindowButtonRestore, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWindowFrameBottom, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWindowFrameLeft, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWindowFrameRight, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWindowTitlebar, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWindowTitlebarMaximized, + + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozGtkInfoBar, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozMacActiveSourceListSelection, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozMacDisclosureButtonClosed, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozMacDisclosureButtonOpen, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozMacSourceList, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozMacSourceListSelection, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozMacVibrancyDark, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozMacVibrancyLight, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozMacVibrantTitlebarDark, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozMacVibrantTitlebarLight, + + /// A non-disappearing scrollbar. + #[css(skip)] + ScrollbarNonDisappearing, + + /// A themed focus outline (for outline:auto). + /// + /// This isn't exposed to CSS at all, just here for convenience. + #[css(skip)] + FocusOutline, + + /// A dummy variant that should be last to let the GTK widget do hackery. + #[css(skip)] + Count, +} + +/// A kind of break between two boxes. +/// +/// https://drafts.csswg.org/css-break/#break-between +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum BreakBetween { + Always, + Auto, + Page, + Avoid, + Left, + Right, +} + +impl BreakBetween { + /// Parse a legacy break-between value for `page-break-*`. + /// + /// See https://drafts.csswg.org/css-break/#page-break-properties. + #[inline] + pub fn parse_legacy<'i>(input: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let ident = input.expect_ident()?; + let break_value = match BreakBetween::from_ident(ident) { + Ok(v) => v, + Err(()) => { + return Err(location + .new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone()))); + }, + }; + match break_value { + BreakBetween::Always => Ok(BreakBetween::Page), + BreakBetween::Auto | BreakBetween::Avoid | BreakBetween::Left | BreakBetween::Right => { + Ok(break_value) + }, + BreakBetween::Page => { + Err(location + .new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone()))) + }, + } + } + + /// Serialize a legacy break-between value for `page-break-*`. + /// + /// See https://drafts.csswg.org/css-break/#page-break-properties. + pub fn to_css_legacy<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + BreakBetween::Auto | BreakBetween::Avoid | BreakBetween::Left | BreakBetween::Right => { + self.to_css(dest) + }, + BreakBetween::Page => dest.write_str("always"), + BreakBetween::Always => Ok(()), + } + } +} + +/// A kind of break within a box. +/// +/// https://drafts.csswg.org/css-break/#break-within +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum BreakWithin { + Auto, + Avoid, +} + +/// The value for the `overflow-x` / `overflow-y` properties. +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum Overflow { + Visible, + Hidden, + Scroll, + Auto, + #[cfg(feature = "gecko")] + #[parse(aliases = "-moz-hidden-unscrollable")] + Clip, +} + +impl Overflow { + /// Return true if the value will create a scrollable box. + #[inline] + pub fn is_scrollable(&self) -> bool { + matches!(*self, Self::Hidden | Self::Scroll | Self::Auto) + } + /// Convert the value to a scrollable value if it's not already scrollable. + /// This maps `visible` to `auto` and `clip` to `hidden`. + #[inline] + pub fn to_scrollable(&self) -> Self { + match *self { + Self::Hidden | Self::Scroll | Self::Auto => *self, + Self::Visible => Self::Auto, + #[cfg(feature = "gecko")] + Self::Clip => Self::Hidden, + } + } +} diff --git a/servo/components/style/values/specified/calc.rs b/servo/components/style/values/specified/calc.rs new file mode 100644 index 0000000000..eb8b2fe421 --- /dev/null +++ b/servo/components/style/values/specified/calc.rs @@ -0,0 +1,644 @@ +/* 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/. */ + +//! [Calc expressions][calc]. +//! +//! [calc]: https://drafts.csswg.org/css-values/#calc-notation + +use crate::parser::ParserContext; +use crate::values::generics::calc as generic; +use crate::values::generics::calc::{MinMaxOp, SortKey}; +use crate::values::specified::length::ViewportPercentageLength; +use crate::values::specified::length::{AbsoluteLength, FontRelativeLength, NoCalcLength}; +use crate::values::specified::{self, Angle, Time}; +use crate::values::{CSSFloat, CSSInteger}; +use cssparser::{AngleOrNumber, CowRcStr, NumberOrPercentage, Parser, Token}; +use smallvec::SmallVec; +use std::cmp; +use std::fmt::{self, Write}; +use style_traits::values::specified::AllowedNumericType; +use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss}; + +/// The name of the mathematical function that we're parsing. +#[derive(Clone, Copy, Debug)] +pub enum MathFunction { + /// `calc()`: https://drafts.csswg.org/css-values-4/#funcdef-calc + Calc, + /// `min()`: https://drafts.csswg.org/css-values-4/#funcdef-min + Min, + /// `max()`: https://drafts.csswg.org/css-values-4/#funcdef-max + Max, + /// `clamp()`: https://drafts.csswg.org/css-values-4/#funcdef-clamp + Clamp, +} + +/// A leaf node inside a `Calc` expression's AST. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub enum Leaf { + /// `<length>` + Length(NoCalcLength), + /// `<angle>` + Angle(Angle), + /// `<time>` + Time(Time), + /// `<percentage>` + Percentage(CSSFloat), + /// `<number>` + Number(CSSFloat), +} + +impl ToCss for Leaf { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + Self::Length(ref l) => l.to_css(dest), + Self::Number(ref n) => n.to_css(dest), + Self::Percentage(p) => crate::values::serialize_percentage(p, dest), + Self::Angle(ref a) => a.to_css(dest), + Self::Time(ref t) => t.to_css(dest), + } + } +} + +/// An expected unit we intend to parse within a `calc()` expression. +/// +/// This is used as a hint for the parser to fast-reject invalid expressions. +#[derive(Clone, Copy, PartialEq)] +enum CalcUnit { + /// `<number>` + Number, + /// `<length>` + Length, + /// `<percentage>` + Percentage, + /// `<length> | <percentage>` + LengthPercentage, + /// `<angle>` + Angle, + /// `<time>` + Time, +} + +/// A struct to hold a simplified `<length>` or `<percentage>` expression. +/// +/// In some cases, e.g. DOMMatrix, we support calc(), but reject all the +/// relative lengths, and to_computed_pixel_length_without_context() handles +/// this case. Therefore, if you want to add a new field, please make sure this +/// function work properly. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)] +#[allow(missing_docs)] +pub struct CalcLengthPercentage { + #[css(skip)] + pub clamping_mode: AllowedNumericType, + pub node: CalcNode, +} + +impl SpecifiedValueInfo for CalcLengthPercentage {} + +impl PartialOrd for Leaf { + fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> { + use self::Leaf::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return None; + } + + match (self, other) { + (&Length(ref one), &Length(ref other)) => one.partial_cmp(other), + (&Percentage(ref one), &Percentage(ref other)) => one.partial_cmp(other), + (&Angle(ref one), &Angle(ref other)) => one.degrees().partial_cmp(&other.degrees()), + (&Time(ref one), &Time(ref other)) => one.seconds().partial_cmp(&other.seconds()), + (&Number(ref one), &Number(ref other)) => one.partial_cmp(other), + _ => { + match *self { + Length(..) | Percentage(..) | Angle(..) | Time(..) | Number(..) => {}, + } + unsafe { + debug_unreachable!("Forgot a branch?"); + } + }, + } + } +} + +impl generic::CalcNodeLeaf for Leaf { + fn is_negative(&self) -> bool { + match *self { + Self::Length(ref l) => l.is_negative(), + Self::Percentage(n) | Self::Number(n) => n < 0., + Self::Angle(ref a) => a.degrees() < 0., + Self::Time(ref t) => t.seconds() < 0., + } + } + + fn mul_by(&mut self, scalar: f32) { + match *self { + Self::Length(ref mut l) => { + // FIXME: For consistency this should probably convert absolute + // lengths into pixels. + *l = *l * scalar; + }, + Self::Number(ref mut n) => { + *n *= scalar; + }, + Self::Angle(ref mut a) => { + *a = Angle::from_calc(a.degrees() * scalar); + }, + Self::Time(ref mut t) => { + *t = Time::from_calc(t.seconds() * scalar); + }, + Self::Percentage(ref mut p) => { + *p *= scalar; + }, + } + } + + fn sort_key(&self) -> SortKey { + match *self { + Self::Number(..) => SortKey::Number, + Self::Percentage(..) => SortKey::Percentage, + Self::Time(..) => SortKey::Sec, + Self::Angle(..) => SortKey::Deg, + Self::Length(ref l) => match *l { + NoCalcLength::Absolute(..) => SortKey::Px, + NoCalcLength::FontRelative(ref relative) => match *relative { + FontRelativeLength::Ch(..) => SortKey::Ch, + FontRelativeLength::Em(..) => SortKey::Em, + FontRelativeLength::Ex(..) => SortKey::Ex, + FontRelativeLength::Rem(..) => SortKey::Rem, + }, + NoCalcLength::ViewportPercentage(ref vp) => match *vp { + ViewportPercentageLength::Vh(..) => SortKey::Vh, + ViewportPercentageLength::Vw(..) => SortKey::Vw, + ViewportPercentageLength::Vmax(..) => SortKey::Vmax, + ViewportPercentageLength::Vmin(..) => SortKey::Vmin, + }, + NoCalcLength::ServoCharacterWidth(..) => unreachable!(), + }, + } + } + + fn simplify(&mut self) { + if let Self::Length(NoCalcLength::Absolute(ref mut abs)) = *self { + *abs = AbsoluteLength::Px(abs.to_px()); + } + } + + /// Tries to merge one sum to another, that is, perform `x` + `y`. + /// + /// Only handles leaf nodes, it's the caller's responsibility to simplify + /// them before calling this if needed. + fn try_sum_in_place(&mut self, other: &Self) -> Result<(), ()> { + use self::Leaf::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return Err(()); + } + + match (self, other) { + (&mut Number(ref mut one), &Number(ref other)) | + (&mut Percentage(ref mut one), &Percentage(ref other)) => { + *one += *other; + }, + (&mut Angle(ref mut one), &Angle(ref other)) => { + *one = specified::Angle::from_calc(one.degrees() + other.degrees()); + }, + (&mut Time(ref mut one), &Time(ref other)) => { + *one = specified::Time::from_calc(one.seconds() + other.seconds()); + }, + (&mut Length(ref mut one), &Length(ref other)) => { + *one = one.try_sum(other)?; + }, + _ => { + match *other { + Number(..) | Percentage(..) | Angle(..) | Time(..) | Length(..) => {}, + } + unsafe { + debug_unreachable!(); + } + }, + } + + Ok(()) + } +} + +/// A calc node representation for specified values. +pub type CalcNode = generic::GenericCalcNode<Leaf>; + +impl CalcNode { + /// Tries to parse a single element in the expression, that is, a + /// `<length>`, `<angle>`, `<time>`, `<percentage>`, according to + /// `expected_unit`. + /// + /// May return a "complex" `CalcNode`, in the presence of a parenthesized + /// expression, for example. + fn parse_one<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + expected_unit: CalcUnit, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + match (input.next()?, expected_unit) { + (&Token::Number { value, .. }, _) => Ok(CalcNode::Leaf(Leaf::Number(value))), + ( + &Token::Dimension { + value, ref unit, .. + }, + CalcUnit::Length, + ) | + ( + &Token::Dimension { + value, ref unit, .. + }, + CalcUnit::LengthPercentage, + ) => match NoCalcLength::parse_dimension(context, value, unit) { + Ok(l) => Ok(CalcNode::Leaf(Leaf::Length(l))), + Err(()) => Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + }, + ( + &Token::Dimension { + value, ref unit, .. + }, + CalcUnit::Angle, + ) => { + match Angle::parse_dimension(value, unit, /* from_calc = */ true) { + Ok(a) => Ok(CalcNode::Leaf(Leaf::Angle(a))), + Err(()) => { + Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + }, + } + }, + ( + &Token::Dimension { + value, ref unit, .. + }, + CalcUnit::Time, + ) => { + match Time::parse_dimension(value, unit, /* from_calc = */ true) { + Ok(t) => Ok(CalcNode::Leaf(Leaf::Time(t))), + Err(()) => { + Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + }, + } + }, + (&Token::Percentage { unit_value, .. }, CalcUnit::LengthPercentage) | + (&Token::Percentage { unit_value, .. }, CalcUnit::Percentage) => { + Ok(CalcNode::Leaf(Leaf::Percentage(unit_value))) + }, + (&Token::ParenthesisBlock, _) => input.parse_nested_block(|input| { + CalcNode::parse_argument(context, input, expected_unit) + }), + (&Token::Function(ref name), _) => { + let function = CalcNode::math_function(name, location)?; + CalcNode::parse(context, input, function, expected_unit) + }, + (t, _) => Err(location.new_unexpected_token_error(t.clone())), + } + } + + /// Parse a top-level `calc` expression, with all nested sub-expressions. + /// + /// This is in charge of parsing, for example, `2 + 3 * 100%`. + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + function: MathFunction, + expected_unit: CalcUnit, + ) -> Result<Self, ParseError<'i>> { + // TODO: Do something different based on the function name. In + // particular, for non-calc function we need to take a list of + // comma-separated arguments and such. + input.parse_nested_block(|input| { + match function { + MathFunction::Calc => Self::parse_argument(context, input, expected_unit), + MathFunction::Clamp => { + let min = Self::parse_argument(context, input, expected_unit)?; + input.expect_comma()?; + let center = Self::parse_argument(context, input, expected_unit)?; + input.expect_comma()?; + let max = Self::parse_argument(context, input, expected_unit)?; + Ok(Self::Clamp { + min: Box::new(min), + center: Box::new(center), + max: Box::new(max), + }) + }, + MathFunction::Min | MathFunction::Max => { + // TODO(emilio): The common case for parse_comma_separated + // is just one element, but for min / max is two, really... + // + // Consider adding an API to cssparser to specify the + // initial vector capacity? + let arguments = input.parse_comma_separated(|input| { + Self::parse_argument(context, input, expected_unit) + })?; + + let op = match function { + MathFunction::Min => MinMaxOp::Min, + MathFunction::Max => MinMaxOp::Max, + _ => unreachable!(), + }; + + Ok(Self::MinMax(arguments.into(), op)) + }, + } + }) + } + + fn parse_argument<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + expected_unit: CalcUnit, + ) -> Result<Self, ParseError<'i>> { + let mut sum = SmallVec::<[CalcNode; 1]>::new(); + sum.push(Self::parse_product(context, input, expected_unit)?); + + loop { + let start = input.state(); + match input.next_including_whitespace() { + Ok(&Token::WhiteSpace(_)) => { + if input.is_exhausted() { + break; // allow trailing whitespace + } + match *input.next()? { + Token::Delim('+') => { + sum.push(Self::parse_product(context, input, expected_unit)?); + }, + Token::Delim('-') => { + let mut rhs = Self::parse_product(context, input, expected_unit)?; + rhs.negate(); + sum.push(rhs); + }, + ref t => { + let t = t.clone(); + return Err(input.new_unexpected_token_error(t)); + }, + } + }, + _ => { + input.reset(&start); + break; + }, + } + } + + Ok(if sum.len() == 1 { + sum.drain(..).next().unwrap() + } else { + Self::Sum(sum.into_boxed_slice().into()) + }) + } + + /// Parse a top-level `calc` expression, and all the products that may + /// follow, and stop as soon as a non-product expression is found. + /// + /// This should parse correctly: + /// + /// * `2` + /// * `2 * 2` + /// * `2 * 2 + 2` (but will leave the `+ 2` unparsed). + /// + fn parse_product<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + expected_unit: CalcUnit, + ) -> Result<Self, ParseError<'i>> { + let mut node = Self::parse_one(context, input, expected_unit)?; + + loop { + let start = input.state(); + match input.next() { + Ok(&Token::Delim('*')) => { + let rhs = Self::parse_one(context, input, expected_unit)?; + if let Ok(rhs) = rhs.to_number() { + node.mul_by(rhs); + } else if let Ok(number) = node.to_number() { + node = rhs; + node.mul_by(number); + } else { + // One of the two parts of the multiplication has to be + // a number, at least until we implement unit math. + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + }, + Ok(&Token::Delim('/')) => { + let rhs = Self::parse_one(context, input, expected_unit)?; + // Dividing by units is not ok. + // + // TODO(emilio): Eventually it should be. + let number = match rhs.to_number() { + Ok(n) if n != 0. => n, + _ => { + return Err( + input.new_custom_error(StyleParseErrorKind::UnspecifiedError) + ); + }, + }; + node.mul_by(1. / number); + }, + _ => { + input.reset(&start); + break; + }, + } + } + + Ok(node) + } + + /// Tries to simplify this expression into a `<length>` or `<percentage>` + /// value. + fn into_length_or_percentage( + mut self, + clamping_mode: AllowedNumericType, + ) -> Result<CalcLengthPercentage, ()> { + // Keep track of whether there's any invalid member of the calculation, + // so as to reject the calculation properly at parse-time. + let mut any_invalid = false; + self.visit_depth_first(|node| { + if let CalcNode::Leaf(ref l) = *node { + any_invalid |= !matches!(*l, Leaf::Percentage(..) | Leaf::Length(..)); + } + node.simplify_and_sort_direct_children(); + }); + + if any_invalid { + return Err(()); + } + + Ok(CalcLengthPercentage { + clamping_mode, + node: self, + }) + } + + /// Tries to simplify this expression into a `<time>` value. + fn to_time(&self) -> Result<Time, ()> { + let seconds = self.resolve(|leaf| match *leaf { + Leaf::Time(ref t) => Ok(t.seconds()), + _ => Err(()), + })?; + Ok(Time::from_calc(crate::values::normalize(seconds))) + } + + /// Tries to simplify this expression into an `Angle` value. + fn to_angle(&self) -> Result<Angle, ()> { + let degrees = self.resolve(|leaf| match *leaf { + Leaf::Angle(ref angle) => Ok(angle.degrees()), + _ => Err(()), + })?; + Ok(Angle::from_calc(crate::values::normalize(degrees))) + } + + /// Tries to simplify this expression into a `<number>` value. + fn to_number(&self) -> Result<CSSFloat, ()> { + self.resolve(|leaf| match *leaf { + Leaf::Number(n) => Ok(n), + _ => Err(()), + }) + } + + /// Tries to simplify this expression into a `<percentage>` value. + fn to_percentage(&self) -> Result<CSSFloat, ()> { + self.resolve(|leaf| match *leaf { + Leaf::Percentage(p) => Ok(p), + _ => Err(()), + }) + } + + /// Given a function name, and the location from where the token came from, + /// return a mathematical function corresponding to that name or an error. + #[inline] + pub fn math_function<'i>( + name: &CowRcStr<'i>, + location: cssparser::SourceLocation, + ) -> Result<MathFunction, ParseError<'i>> { + Ok(match_ignore_ascii_case! { &*name, + "calc" => MathFunction::Calc, + "min" => MathFunction::Min, + "max" => MathFunction::Max, + "clamp" => MathFunction::Clamp, + _ => return Err(location.new_unexpected_token_error(Token::Function(name.clone()))), + }) + } + + /// Convenience parsing function for integers. + pub fn parse_integer<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + function: MathFunction, + ) -> Result<CSSInteger, ParseError<'i>> { + Self::parse_number(context, input, function).map(|n| n.round() as CSSInteger) + } + + /// Convenience parsing function for `<length> | <percentage>`. + pub fn parse_length_or_percentage<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + clamping_mode: AllowedNumericType, + function: MathFunction, + ) -> Result<CalcLengthPercentage, ParseError<'i>> { + Self::parse(context, input, function, CalcUnit::LengthPercentage)? + .into_length_or_percentage(clamping_mode) + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + /// Convenience parsing function for percentages. + pub fn parse_percentage<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + function: MathFunction, + ) -> Result<CSSFloat, ParseError<'i>> { + Self::parse(context, input, function, CalcUnit::Percentage)? + .to_percentage() + .map(crate::values::normalize) + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + /// Convenience parsing function for `<length>`. + pub fn parse_length<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + clamping_mode: AllowedNumericType, + function: MathFunction, + ) -> Result<CalcLengthPercentage, ParseError<'i>> { + Self::parse(context, input, function, CalcUnit::Length)? + .into_length_or_percentage(clamping_mode) + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + /// Convenience parsing function for `<number>`. + pub fn parse_number<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + function: MathFunction, + ) -> Result<CSSFloat, ParseError<'i>> { + Self::parse(context, input, function, CalcUnit::Number)? + .to_number() + .map(crate::values::normalize) + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + /// Convenience parsing function for `<angle>`. + pub fn parse_angle<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + function: MathFunction, + ) -> Result<Angle, ParseError<'i>> { + Self::parse(context, input, function, CalcUnit::Angle)? + .to_angle() + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + /// Convenience parsing function for `<time>`. + pub fn parse_time<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + function: MathFunction, + ) -> Result<Time, ParseError<'i>> { + Self::parse(context, input, function, CalcUnit::Time)? + .to_time() + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + /// Convenience parsing function for `<number>` or `<percentage>`. + pub fn parse_number_or_percentage<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + function: MathFunction, + ) -> Result<NumberOrPercentage, ParseError<'i>> { + let node = Self::parse(context, input, function, CalcUnit::Percentage)?; + + if let Ok(value) = node.to_number() { + return Ok(NumberOrPercentage::Number { value }); + } + + match node.to_percentage() { + Ok(unit_value) => Ok(NumberOrPercentage::Percentage { unit_value }), + Err(()) => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } + } + + /// Convenience parsing function for `<number>` or `<angle>`. + pub fn parse_angle_or_number<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + function: MathFunction, + ) -> Result<AngleOrNumber, ParseError<'i>> { + let node = Self::parse(context, input, function, CalcUnit::Angle)?; + + if let Ok(angle) = node.to_angle() { + let degrees = angle.degrees(); + return Ok(AngleOrNumber::Angle { degrees }); + } + + match node.to_number() { + Ok(value) => Ok(AngleOrNumber::Number { value }), + Err(()) => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } + } +} diff --git a/servo/components/style/values/specified/color.rs b/servo/components/style/values/specified/color.rs new file mode 100644 index 0000000000..e44568fb5b --- /dev/null +++ b/servo/components/style/values/specified/color.rs @@ -0,0 +1,665 @@ +/* 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/. */ + +//! Specified color values. + +use super::AllowQuirks; +#[cfg(feature = "gecko")] +use crate::gecko_bindings::structs::nscolor; +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::{Color as ComputedColor, Context, ToComputedValue}; +use crate::values::generics::color::{Color as GenericColor, ColorOrAuto as GenericColorOrAuto}; +use crate::values::specified::calc::CalcNode; +use cssparser::{AngleOrNumber, Color as CSSParserColor, Parser, Token, RGBA}; +use cssparser::{BasicParseErrorKind, NumberOrPercentage, ParseErrorKind}; +use itoa; +use std::fmt::{self, Write}; +use std::io::Write as IoWrite; +use style_traits::{CssType, CssWriter, KeywordsCollectFn, ParseError, StyleParseErrorKind}; +use style_traits::{SpecifiedValueInfo, ToCss, ValueParseErrorKind}; + +/// Specified color value +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub enum Color { + /// The 'currentColor' keyword + CurrentColor, + /// A specific RGBA color + Numeric { + /// Parsed RGBA color + parsed: RGBA, + /// Authored representation + authored: Option<Box<str>>, + }, + /// A complex color value from computed value + Complex(ComputedColor), + /// A system color + #[cfg(feature = "gecko")] + System(SystemColor), + /// Quirksmode-only rule for inheriting color from the body + #[cfg(feature = "gecko")] + InheritFromBodyQuirk, +} + +/// System colors. +#[allow(missing_docs)] +#[cfg(feature = "gecko")] +#[derive(Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)] +#[repr(u8)] +pub enum SystemColor { + #[css(skip)] + WindowBackground, + #[css(skip)] + WindowForeground, + #[css(skip)] + WidgetBackground, + #[css(skip)] + WidgetForeground, + #[css(skip)] + WidgetSelectBackground, + #[css(skip)] + WidgetSelectForeground, + #[css(skip)] + Widget3DHighlight, + #[css(skip)] + Widget3DShadow, + #[css(skip)] + TextBackground, + #[css(skip)] + TextForeground, + #[css(skip)] + TextSelectBackground, + #[css(skip)] + TextSelectForeground, + #[css(skip)] + TextSelectForegroundCustom, + #[css(skip)] + TextSelectBackgroundDisabled, + #[css(skip)] + TextSelectBackgroundAttention, + #[css(skip)] + TextHighlightBackground, + #[css(skip)] + TextHighlightForeground, + #[css(skip)] + IMERawInputBackground, + #[css(skip)] + IMERawInputForeground, + #[css(skip)] + IMERawInputUnderline, + #[css(skip)] + IMESelectedRawTextBackground, + #[css(skip)] + IMESelectedRawTextForeground, + #[css(skip)] + IMESelectedRawTextUnderline, + #[css(skip)] + IMEConvertedTextBackground, + #[css(skip)] + IMEConvertedTextForeground, + #[css(skip)] + IMEConvertedTextUnderline, + #[css(skip)] + IMESelectedConvertedTextBackground, + #[css(skip)] + IMESelectedConvertedTextForeground, + #[css(skip)] + IMESelectedConvertedTextUnderline, + #[css(skip)] + SpellCheckerUnderline, + #[css(skip)] + ThemedScrollbar, + #[css(skip)] + ThemedScrollbarInactive, + #[css(skip)] + ThemedScrollbarThumb, + #[css(skip)] + ThemedScrollbarThumbHover, + #[css(skip)] + ThemedScrollbarThumbActive, + #[css(skip)] + ThemedScrollbarThumbInactive, + Activeborder, + Activecaption, + Appworkspace, + Background, + Buttonface, + Buttonhighlight, + Buttonshadow, + Buttontext, + Captiontext, + #[parse(aliases = "-moz-field")] + Field, + #[parse(aliases = "-moz-fieldtext")] + Fieldtext, + Graytext, + Highlight, + Highlighttext, + Inactiveborder, + Inactivecaption, + Inactivecaptiontext, + Infobackground, + Infotext, + Menu, + Menutext, + Scrollbar, + Threeddarkshadow, + Threedface, + Threedhighlight, + Threedlightshadow, + Threedshadow, + Window, + Windowframe, + Windowtext, + MozButtondefault, + #[parse(aliases = "-moz-default-color")] + Canvastext, + #[parse(aliases = "-moz-default-background-color")] + Canvas, + MozDialog, + MozDialogtext, + /// Used to highlight valid regions to drop something onto. + MozDragtargetzone, + /// Used for selected but not focused cell backgrounds. + MozCellhighlight, + /// Used for selected but not focused cell text. + MozCellhighlighttext, + /// Used for selected but not focused html cell backgrounds. + MozHtmlCellhighlight, + /// Used for selected but not focused html cell text. + MozHtmlCellhighlighttext, + /// Used to button text background when hovered. + MozButtonhoverface, + /// Used to button text color when hovered. + MozButtonhovertext, + /// Used for menu item backgrounds when hovered. + MozMenuhover, + /// Used for menu item text when hovered. + MozMenuhovertext, + /// Used for menubar item text. + MozMenubartext, + /// Used for menubar item text when hovered. + MozMenubarhovertext, + + /// On platforms where these colors are the same as -moz-field, use + /// -moz-fieldtext as foreground color + MozEventreerow, + MozOddtreerow, + + /// Used for button text when pressed. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozGtkButtonactivetext, + + /// Used for button text when pressed. + MozMacButtonactivetext, + /// Background color of chrome toolbars in active windows. + MozMacChromeActive, + /// Background color of chrome toolbars in inactive windows. + MozMacChromeInactive, + /// Foreground color of default buttons. + MozMacDefaultbuttontext, + /// Ring color around text fields and lists. + MozMacFocusring, + /// Color used when mouse is over a menu item. + MozMacMenuselect, + /// Color used to do shadows on menu items. + MozMacMenushadow, + /// Color used to display text for disabled menu items. + MozMacMenutextdisable, + /// Color used to display text while mouse is over a menu item. + MozMacMenutextselect, + /// Text color of disabled text on toolbars. + MozMacDisabledtoolbartext, + /// Inactive light hightlight + MozMacSecondaryhighlight, + + /// Font smoothing background colors needed by the Mac OS X theme, based on + /// -moz-appearance names. + MozMacVibrancyLight, + MozMacVibrancyDark, + MozMacVibrantTitlebarLight, + MozMacVibrantTitlebarDark, + MozMacMenupopup, + MozMacMenuitem, + MozMacActiveMenuitem, + MozMacSourceList, + MozMacSourceListSelection, + MozMacActiveSourceListSelection, + MozMacTooltip, + + /// Accent color for title bar. + MozWinAccentcolor, + /// Color from drawing text over the accent color. + MozWinAccentcolortext, + /// Media rebar text. + MozWinMediatext, + /// Communications rebar text. + MozWinCommunicationstext, + + /// Hyperlink color extracted from the system, not affected by the + /// browser.anchor_color user pref. + /// + /// There is no OS-specified safe background color for this text, but it is + /// used regularly within Windows and the Gnome DE on Dialog and Window + /// colors. + MozNativehyperlinktext, + + #[parse(aliases = "-moz-hyperlinktext")] + Linktext, + #[parse(aliases = "-moz-activehyperlinktext")] + Activetext, + #[parse(aliases = "-moz-visitedhyperlinktext")] + Visitedtext, + + /// Combobox widgets + MozComboboxtext, + MozCombobox, + + MozGtkInfoBarText, + + /// Color of tree column headers + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozColheadertext, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozColheaderhovertext, + + #[css(skip)] + End, // Just for array-indexing purposes. +} + +#[cfg(feature = "gecko")] +impl SystemColor { + #[inline] + fn compute(&self, cx: &Context) -> ComputedColor { + use crate::gecko_bindings::bindings; + + let prefs = cx.device().pref_sheet_prefs(); + + convert_nscolor_to_computedcolor(match *self { + SystemColor::Canvastext => prefs.mDefaultColor, + SystemColor::Canvas => prefs.mDefaultBackgroundColor, + SystemColor::Linktext => prefs.mLinkColor, + SystemColor::Activetext => prefs.mActiveLinkColor, + SystemColor::Visitedtext => prefs.mVisitedLinkColor, + + _ => unsafe { + bindings::Gecko_GetLookAndFeelSystemColor(*self as i32, cx.device().document()) + }, + }) + } +} + +impl From<RGBA> for Color { + fn from(value: RGBA) -> Self { + Color::rgba(value) + } +} + +struct ColorComponentParser<'a, 'b: 'a>(&'a ParserContext<'b>); +impl<'a, 'b: 'a, 'i: 'a> ::cssparser::ColorComponentParser<'i> for ColorComponentParser<'a, 'b> { + type Error = StyleParseErrorKind<'i>; + + fn parse_angle_or_number<'t>( + &self, + input: &mut Parser<'i, 't>, + ) -> Result<AngleOrNumber, ParseError<'i>> { + use crate::values::specified::Angle; + + let location = input.current_source_location(); + let token = input.next()?.clone(); + match token { + Token::Dimension { + value, ref unit, .. + } => { + let angle = Angle::parse_dimension(value, unit, /* from_calc = */ false); + + let degrees = match angle { + Ok(angle) => angle.degrees(), + Err(()) => return Err(location.new_unexpected_token_error(token.clone())), + }; + + Ok(AngleOrNumber::Angle { degrees }) + }, + Token::Number { value, .. } => Ok(AngleOrNumber::Number { value }), + Token::Function(ref name) => { + let function = CalcNode::math_function(name, location)?; + CalcNode::parse_angle_or_number(self.0, input, function) + }, + t => return Err(location.new_unexpected_token_error(t)), + } + } + + fn parse_percentage<'t>(&self, input: &mut Parser<'i, 't>) -> Result<f32, ParseError<'i>> { + use crate::values::specified::Percentage; + + Ok(Percentage::parse(self.0, input)?.get()) + } + + fn parse_number<'t>(&self, input: &mut Parser<'i, 't>) -> Result<f32, ParseError<'i>> { + use crate::values::specified::Number; + + Ok(Number::parse(self.0, input)?.get()) + } + + fn parse_number_or_percentage<'t>( + &self, + input: &mut Parser<'i, 't>, + ) -> Result<NumberOrPercentage, ParseError<'i>> { + let location = input.current_source_location(); + + match *input.next()? { + Token::Number { value, .. } => Ok(NumberOrPercentage::Number { value }), + Token::Percentage { unit_value, .. } => { + Ok(NumberOrPercentage::Percentage { unit_value }) + }, + Token::Function(ref name) => { + let function = CalcNode::math_function(name, location)?; + CalcNode::parse_number_or_percentage(self.0, input, function) + }, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + } + } +} + +impl Parse for Color { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // Currently we only store authored value for color keywords, + // because all browsers serialize those values as keywords for + // specified value. + let start = input.state(); + let authored = input.expect_ident_cloned().ok(); + input.reset(&start); + + let compontent_parser = ColorComponentParser(&*context); + match input.try_parse(|i| CSSParserColor::parse_with(&compontent_parser, i)) { + Ok(value) => Ok(match value { + CSSParserColor::CurrentColor => Color::CurrentColor, + CSSParserColor::RGBA(rgba) => Color::Numeric { + parsed: rgba, + authored: authored.map(|s| s.to_ascii_lowercase().into_boxed_str()), + }, + }), + Err(e) => { + #[cfg(feature = "gecko")] + { + if let Ok(system) = input.try_parse(|i| SystemColor::parse(context, i)) { + return Ok(Color::System(system)); + } + } + + match e.kind { + ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(t)) => { + Err(e.location.new_custom_error(StyleParseErrorKind::ValueError( + ValueParseErrorKind::InvalidColor(t), + ))) + }, + _ => Err(e), + } + }, + } + } +} + +impl ToCss for Color { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + Color::CurrentColor => CSSParserColor::CurrentColor.to_css(dest), + Color::Numeric { + authored: Some(ref authored), + .. + } => dest.write_str(authored), + Color::Numeric { + parsed: ref rgba, .. + } => rgba.to_css(dest), + Color::Complex(_) => Ok(()), + #[cfg(feature = "gecko")] + Color::System(system) => system.to_css(dest), + #[cfg(feature = "gecko")] + Color::InheritFromBodyQuirk => Ok(()), + } + } +} + +/// A wrapper of cssparser::Color::parse_hash. +/// +/// That function should never return CurrentColor, so it makes no sense to +/// handle a cssparser::Color here. This should really be done in cssparser +/// directly rather than here. +fn parse_hash_color(value: &[u8]) -> Result<RGBA, ()> { + CSSParserColor::parse_hash(value).map(|color| match color { + CSSParserColor::RGBA(rgba) => rgba, + CSSParserColor::CurrentColor => unreachable!("parse_hash should never return currentcolor"), + }) +} + +impl Color { + /// Returns currentcolor value. + #[inline] + pub fn currentcolor() -> Color { + Color::CurrentColor + } + + /// Returns transparent value. + #[inline] + pub fn transparent() -> Color { + // We should probably set authored to "transparent", but maybe it doesn't matter. + Color::rgba(RGBA::transparent()) + } + + /// Returns a numeric RGBA color value. + #[inline] + pub fn rgba(rgba: RGBA) -> Self { + Color::Numeric { + parsed: rgba, + authored: None, + } + } + + /// Parse a color, with quirks. + /// + /// <https://quirks.spec.whatwg.org/#the-hashless-hex-color-quirk> + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + input.try_parse(|i| Self::parse(context, i)).or_else(|e| { + if !allow_quirks.allowed(context.quirks_mode) { + return Err(e); + } + Color::parse_quirky_color(input) + .map(Color::rgba) + .map_err(|_| e) + }) + } + + /// Parse a <quirky-color> value. + /// + /// <https://quirks.spec.whatwg.org/#the-hashless-hex-color-quirk> + fn parse_quirky_color<'i, 't>(input: &mut Parser<'i, 't>) -> Result<RGBA, ParseError<'i>> { + let location = input.current_source_location(); + let (value, unit) = match *input.next()? { + Token::Number { + int_value: Some(integer), + .. + } => (integer, None), + Token::Dimension { + int_value: Some(integer), + ref unit, + .. + } => (integer, Some(unit)), + Token::Ident(ref ident) => { + if ident.len() != 3 && ident.len() != 6 { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + return parse_hash_color(ident.as_bytes()).map_err(|()| { + location.new_custom_error(StyleParseErrorKind::UnspecifiedError) + }); + }, + ref t => { + return Err(location.new_unexpected_token_error(t.clone())); + }, + }; + if value < 0 { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + let length = if value <= 9 { + 1 + } else if value <= 99 { + 2 + } else if value <= 999 { + 3 + } else if value <= 9999 { + 4 + } else if value <= 99999 { + 5 + } else if value <= 999999 { + 6 + } else { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + }; + let total = length + unit.as_ref().map_or(0, |d| d.len()); + if total > 6 { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + let mut serialization = [b'0'; 6]; + let space_padding = 6 - total; + let mut written = space_padding; + written += itoa::write(&mut serialization[written..], value).unwrap(); + if let Some(unit) = unit { + written += (&mut serialization[written..]) + .write(unit.as_bytes()) + .unwrap(); + } + debug_assert_eq!(written, 6); + parse_hash_color(&serialization) + .map_err(|()| location.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } +} + +#[cfg(feature = "gecko")] +fn convert_nscolor_to_computedcolor(color: nscolor) -> ComputedColor { + use crate::gecko::values::convert_nscolor_to_rgba; + ComputedColor::rgba(convert_nscolor_to_rgba(color)) +} + +impl Color { + /// Converts this Color into a ComputedColor. + /// + /// If `context` is `None`, and the specified color requires data from + /// the context to resolve, then `None` is returned. + pub fn to_computed_color(&self, _context: Option<&Context>) -> Option<ComputedColor> { + Some(match *self { + Color::CurrentColor => ComputedColor::currentcolor(), + Color::Numeric { ref parsed, .. } => ComputedColor::rgba(*parsed), + Color::Complex(ref complex) => *complex, + #[cfg(feature = "gecko")] + Color::System(system) => system.compute(_context?), + #[cfg(feature = "gecko")] + Color::InheritFromBodyQuirk => { + ComputedColor::rgba(_context?.device().body_text_color()) + }, + }) + } +} + +impl ToComputedValue for Color { + type ComputedValue = ComputedColor; + + fn to_computed_value(&self, context: &Context) -> ComputedColor { + self.to_computed_color(Some(context)).unwrap() + } + + fn from_computed_value(computed: &ComputedColor) -> Self { + match *computed { + GenericColor::Numeric(color) => Color::rgba(color), + GenericColor::CurrentColor => Color::currentcolor(), + GenericColor::Complex { .. } => Color::Complex(*computed), + } + } +} + +/// Specified color value for `-moz-font-smoothing-background-color`. +/// +/// This property does not support `currentcolor`. We could drop it at +/// parse-time, but it's not exposed to the web so it doesn't really matter. +/// +/// We resolve it to `transparent` instead. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub struct MozFontSmoothingBackgroundColor(pub Color); + +impl Parse for MozFontSmoothingBackgroundColor { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Color::parse(context, input).map(MozFontSmoothingBackgroundColor) + } +} + +impl ToComputedValue for MozFontSmoothingBackgroundColor { + type ComputedValue = RGBA; + + fn to_computed_value(&self, context: &Context) -> RGBA { + self.0 + .to_computed_value(context) + .to_rgba(RGBA::transparent()) + } + + fn from_computed_value(computed: &RGBA) -> Self { + MozFontSmoothingBackgroundColor(Color::rgba(*computed)) + } +} + +impl SpecifiedValueInfo for Color { + const SUPPORTED_TYPES: u8 = CssType::COLOR; + + fn collect_completion_keywords(f: KeywordsCollectFn) { + // We are not going to insert all the color names here. Caller and + // devtools should take care of them. XXX Actually, transparent + // should probably be handled that way as well. + // XXX `currentColor` should really be `currentcolor`. But let's + // keep it consistent with the old system for now. + f(&["rgb", "rgba", "hsl", "hsla", "currentColor", "transparent"]); + } +} + +/// Specified value for the "color" property, which resolves the `currentcolor` +/// keyword to the parent color instead of self's color. +#[cfg_attr(feature = "gecko", derive(MallocSizeOf))] +#[derive(Clone, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub struct ColorPropertyValue(pub Color); + +impl ToComputedValue for ColorPropertyValue { + type ComputedValue = RGBA; + + #[inline] + fn to_computed_value(&self, context: &Context) -> RGBA { + self.0 + .to_computed_value(context) + .to_rgba(context.builder.get_parent_inherited_text().clone_color()) + } + + #[inline] + fn from_computed_value(computed: &RGBA) -> Self { + ColorPropertyValue(Color::rgba(*computed).into()) + } +} + +impl Parse for ColorPropertyValue { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Color::parse_quirky(context, input, AllowQuirks::Yes).map(ColorPropertyValue) + } +} + +/// auto | <color> +pub type ColorOrAuto = GenericColorOrAuto<Color>; diff --git a/servo/components/style/values/specified/column.rs b/servo/components/style/values/specified/column.rs new file mode 100644 index 0000000000..2dd7bb0144 --- /dev/null +++ b/servo/components/style/values/specified/column.rs @@ -0,0 +1,11 @@ +/* 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/. */ + +//! Specified types for the column properties. + +use crate::values::generics::column::ColumnCount as GenericColumnCount; +use crate::values::specified::PositiveInteger; + +/// A specified type for `column-count` values. +pub type ColumnCount = GenericColumnCount<PositiveInteger>; diff --git a/servo/components/style/values/specified/counters.rs b/servo/components/style/values/specified/counters.rs new file mode 100644 index 0000000000..ac097c91a9 --- /dev/null +++ b/servo/components/style/values/specified/counters.rs @@ -0,0 +1,211 @@ +/* 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/. */ + +//! Specified types for counter properties. + +#[cfg(feature = "servo")] +use crate::computed_values::list_style_type::T as ListStyleType; +use crate::parser::{Parse, ParserContext}; +use crate::values::generics::counters as generics; +use crate::values::generics::counters::CounterPair; +#[cfg(feature = "gecko")] +use crate::values::generics::CounterStyle; +use crate::values::specified::url::SpecifiedImageUrl; +#[cfg(any(feature = "gecko", feature = "servo-layout-2020"))] +use crate::values::specified::Attr; +use crate::values::specified::Integer; +use crate::values::CustomIdent; +use cssparser::{Parser, Token}; +use selectors::parser::SelectorParseErrorKind; +use style_traits::{ParseError, StyleParseErrorKind}; + +/// A specified value for the `counter-increment` property. +pub type CounterIncrement = generics::GenericCounterIncrement<Integer>; + +impl Parse for CounterIncrement { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(Self::new(parse_counters(context, input, 1)?)) + } +} + +/// A specified value for the `counter-set` and `counter-reset` properties. +pub type CounterSetOrReset = generics::GenericCounterSetOrReset<Integer>; + +impl Parse for CounterSetOrReset { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(Self::new(parse_counters(context, input, 0)?)) + } +} + +fn parse_counters<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + default_value: i32, +) -> Result<Vec<CounterPair<Integer>>, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("none")) + .is_ok() + { + return Ok(vec![]); + } + + let mut counters = Vec::new(); + loop { + let location = input.current_source_location(); + let name = match input.next() { + Ok(&Token::Ident(ref ident)) => CustomIdent::from_ident(location, ident, &["none"])?, + Ok(t) => { + let t = t.clone(); + return Err(location.new_unexpected_token_error(t)); + }, + Err(_) => break, + }; + + let value = input + .try_parse(|input| Integer::parse(context, input)) + .unwrap_or(Integer::new(default_value)); + counters.push(CounterPair { name, value }); + } + + if !counters.is_empty() { + Ok(counters) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } +} + +/// The specified value for the `content` property. +pub type Content = generics::GenericContent<SpecifiedImageUrl>; + +/// The specified value for a content item in the `content` property. +pub type ContentItem = generics::GenericContentItem<SpecifiedImageUrl>; + +impl Content { + #[cfg(feature = "servo")] + fn parse_counter_style(_: &ParserContext, input: &mut Parser) -> ListStyleType { + input + .try_parse(|input| { + input.expect_comma()?; + ListStyleType::parse(input) + }) + .unwrap_or(ListStyleType::Decimal) + } + + #[cfg(feature = "gecko")] + fn parse_counter_style(context: &ParserContext, input: &mut Parser) -> CounterStyle { + input + .try_parse(|input| { + input.expect_comma()?; + CounterStyle::parse(context, input) + }) + .unwrap_or(CounterStyle::decimal()) + } +} + +impl Parse for Content { + // normal | none | [ <string> | <counter> | open-quote | close-quote | no-open-quote | + // no-close-quote ]+ + // TODO: <uri>, attr(<identifier>) + #[cfg_attr(feature = "servo", allow(unused_mut))] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("normal")) + .is_ok() + { + return Ok(generics::Content::Normal); + } + if input + .try_parse(|input| input.expect_ident_matching("none")) + .is_ok() + { + return Ok(generics::Content::None); + } + + let mut content = vec![]; + let mut has_alt_content = false; + loop { + #[cfg(any(feature = "gecko", feature = "servo-layout-2020"))] + { + if let Ok(url) = input.try_parse(|i| SpecifiedImageUrl::parse(context, i)) { + content.push(generics::ContentItem::Url(url)); + continue; + } + } + match input.next() { + Ok(&Token::QuotedString(ref value)) => { + content.push(generics::ContentItem::String( + value.as_ref().to_owned().into(), + )); + }, + Ok(&Token::Function(ref name)) => { + let result = match_ignore_ascii_case! { &name, + "counter" => input.parse_nested_block(|input| { + let location = input.current_source_location(); + let name = CustomIdent::from_ident(location, input.expect_ident()?, &[])?; + let style = Content::parse_counter_style(context, input); + Ok(generics::ContentItem::Counter(name, style)) + }), + "counters" => input.parse_nested_block(|input| { + let location = input.current_source_location(); + let name = CustomIdent::from_ident(location, input.expect_ident()?, &[])?; + input.expect_comma()?; + let separator = input.expect_string()?.as_ref().to_owned().into(); + let style = Content::parse_counter_style(context, input); + Ok(generics::ContentItem::Counters(name, separator, style)) + }), + #[cfg(any(feature = "gecko", feature = "servo-layout-2020"))] + "attr" => input.parse_nested_block(|input| { + Ok(generics::ContentItem::Attr(Attr::parse_function(context, input)?)) + }), + _ => { + let name = name.clone(); + return Err(input.new_custom_error( + StyleParseErrorKind::UnexpectedFunction(name), + )) + } + }?; + content.push(result); + }, + Ok(&Token::Ident(ref ident)) => { + content.push(match_ignore_ascii_case! { &ident, + "open-quote" => generics::ContentItem::OpenQuote, + "close-quote" => generics::ContentItem::CloseQuote, + "no-open-quote" => generics::ContentItem::NoOpenQuote, + "no-close-quote" => generics::ContentItem::NoCloseQuote, + #[cfg(feature = "gecko")] + "-moz-alt-content" => { + has_alt_content = true; + generics::ContentItem::MozAltContent + }, + _ =>{ + let ident = ident.clone(); + return Err(input.new_custom_error( + SelectorParseErrorKind::UnexpectedIdent(ident) + )); + } + }); + }, + Err(_) => break, + Ok(t) => { + let t = t.clone(); + return Err(input.new_unexpected_token_error(t)); + }, + } + } + // We don't allow to parse `-moz-alt-content in multiple positions. + if content.is_empty() || (has_alt_content && content.len() != 1) { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(generics::Content::Items(content.into())) + } +} diff --git a/servo/components/style/values/specified/easing.rs b/servo/components/style/values/specified/easing.rs new file mode 100644 index 0000000000..26b921ebdc --- /dev/null +++ b/servo/components/style/values/specified/easing.rs @@ -0,0 +1,106 @@ +/* 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/. */ + +//! Specified types for CSS Easing functions. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::easing::TimingFunction as ComputedTimingFunction; +use crate::values::generics::easing::TimingFunction as GenericTimingFunction; +use crate::values::generics::easing::{StepPosition, TimingKeyword}; +use crate::values::specified::{Integer, Number}; +use cssparser::Parser; +use selectors::parser::SelectorParseErrorKind; +use style_traits::{ParseError, StyleParseErrorKind}; + +/// A specified timing function. +pub type TimingFunction = GenericTimingFunction<Integer, Number>; + +impl Parse for TimingFunction { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(keyword) = input.try_parse(TimingKeyword::parse) { + return Ok(GenericTimingFunction::Keyword(keyword)); + } + if let Ok(ident) = input.try_parse(|i| i.expect_ident_cloned()) { + let position = match_ignore_ascii_case! { &ident, + "step-start" => StepPosition::Start, + "step-end" => StepPosition::End, + _ => { + return Err(input.new_custom_error( + SelectorParseErrorKind::UnexpectedIdent(ident.clone()) + )); + }, + }; + return Ok(GenericTimingFunction::Steps(Integer::new(1), position)); + } + let location = input.current_source_location(); + let function = input.expect_function()?.clone(); + input.parse_nested_block(move |i| { + (match_ignore_ascii_case! { &function, + "cubic-bezier" => { + let x1 = Number::parse(context, i)?; + i.expect_comma()?; + let y1 = Number::parse(context, i)?; + i.expect_comma()?; + let x2 = Number::parse(context, i)?; + i.expect_comma()?; + let y2 = Number::parse(context, i)?; + + if x1.get() < 0.0 || x1.get() > 1.0 || x2.get() < 0.0 || x2.get() > 1.0 { + return Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(GenericTimingFunction::CubicBezier { x1, y1, x2, y2 }) + }, + "steps" => { + let steps = Integer::parse_positive(context, i)?; + let position = i.try_parse(|i| { + i.expect_comma()?; + StepPosition::parse(context, i) + }).unwrap_or(StepPosition::End); + + // jump-none accepts a positive integer greater than 1. + // FIXME(emilio): The spec asks us to avoid rejecting it at parse + // time except until computed value time. + // + // It's not totally clear it's worth it though, and no other browser + // does this. + if position == StepPosition::JumpNone && 2 > steps.value() { + return Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(GenericTimingFunction::Steps(steps, position)) + }, + _ => Err(()), + }) + .map_err(|()| { + location.new_custom_error(StyleParseErrorKind::UnexpectedFunction(function.clone())) + }) + }) + } +} + +// We need this for converting the specified TimingFunction into computed TimingFunction without +// Context (for some FFIs in glue.rs). In fact, we don't really need Context to get the computed +// value of TimingFunction. +impl TimingFunction { + /// Generate the ComputedTimingFunction without Context. + pub fn to_computed_value_without_context(&self) -> ComputedTimingFunction { + match *self { + GenericTimingFunction::Steps(steps, pos) => { + GenericTimingFunction::Steps(steps.value(), pos) + }, + GenericTimingFunction::CubicBezier { x1, y1, x2, y2 } => { + GenericTimingFunction::CubicBezier { + x1: x1.get(), + y1: y1.get(), + x2: x2.get(), + y2: y2.get(), + } + }, + GenericTimingFunction::Keyword(keyword) => GenericTimingFunction::Keyword(keyword), + } + } +} diff --git a/servo/components/style/values/specified/effects.rs b/servo/components/style/values/specified/effects.rs new file mode 100644 index 0000000000..fd92e6ee95 --- /dev/null +++ b/servo/components/style/values/specified/effects.rs @@ -0,0 +1,362 @@ +/* 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/. */ + +//! Specified types for CSS values related to effects. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::effects::BoxShadow as ComputedBoxShadow; +use crate::values::computed::effects::SimpleShadow as ComputedSimpleShadow; +use crate::values::computed::NonNegativeNumber as ComputedNonNegativeNumber; +use crate::values::computed::ZeroToOneNumber as ComputedZeroToOneNumber; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::generics::effects::BoxShadow as GenericBoxShadow; +use crate::values::generics::effects::Filter as GenericFilter; +use crate::values::generics::effects::SimpleShadow as GenericSimpleShadow; +use crate::values::generics::NonNegative; +use crate::values::specified::color::Color; +use crate::values::specified::length::{Length, NonNegativeLength}; +#[cfg(feature = "gecko")] +use crate::values::specified::url::SpecifiedUrl; +use crate::values::specified::{Angle, Number, NumberOrPercentage}; +#[cfg(feature = "servo")] +use crate::values::Impossible; +use crate::Zero; +use cssparser::{self, BasicParseErrorKind, Parser, Token}; +use style_traits::{ParseError, StyleParseErrorKind, ValueParseErrorKind}; + +/// A specified value for a single shadow of the `box-shadow` property. +pub type BoxShadow = + GenericBoxShadow<Option<Color>, Length, Option<NonNegativeLength>, Option<Length>>; + +/// A specified value for a single `filter`. +#[cfg(feature = "gecko")] +pub type SpecifiedFilter = GenericFilter< + Angle, + NonNegativeFactor, + ZeroToOneFactor, + NonNegativeLength, + SimpleShadow, + SpecifiedUrl, +>; + +/// A specified value for a single `filter`. +#[cfg(feature = "servo")] +pub type SpecifiedFilter = GenericFilter< + Angle, + NonNegativeFactor, + ZeroToOneFactor, + NonNegativeLength, + Impossible, + Impossible, +>; + +pub use self::SpecifiedFilter as Filter; + +/// A value for the `<factor>` parts in `Filter`. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub struct NonNegativeFactor(NumberOrPercentage); + +/// A value for the `<factor>` parts in `Filter` which clamps to one. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub struct ZeroToOneFactor(NumberOrPercentage); + +/// Clamp the value to 1 if the value is over 100%. +#[inline] +fn clamp_to_one(number: NumberOrPercentage) -> NumberOrPercentage { + match number { + NumberOrPercentage::Percentage(percent) => { + NumberOrPercentage::Percentage(percent.clamp_to_hundred()) + }, + NumberOrPercentage::Number(number) => NumberOrPercentage::Number(number.clamp_to_one()), + } +} + +macro_rules! factor_impl_common { + ($ty:ty, $computed_ty:ty) => { + impl $ty { + fn one() -> Self { + Self(NumberOrPercentage::Number(Number::new(1.))) + } + } + + impl ToComputedValue for $ty { + type ComputedValue = $computed_ty; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + use crate::values::computed::NumberOrPercentage; + match self.0.to_computed_value(context) { + NumberOrPercentage::Number(n) => n.into(), + NumberOrPercentage::Percentage(p) => p.0.into(), + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Self(NumberOrPercentage::Number( + ToComputedValue::from_computed_value(&computed.0), + )) + } + } + }; +} +factor_impl_common!(NonNegativeFactor, ComputedNonNegativeNumber); +factor_impl_common!(ZeroToOneFactor, ComputedZeroToOneNumber); + +impl Parse for NonNegativeFactor { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + NumberOrPercentage::parse_non_negative(context, input).map(Self) + } +} + +impl Parse for ZeroToOneFactor { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + NumberOrPercentage::parse_non_negative(context, input) + .map(clamp_to_one) + .map(Self) + } +} + +/// A specified value for the `drop-shadow()` filter. +pub type SimpleShadow = GenericSimpleShadow<Option<Color>, Length, Option<NonNegativeLength>>; + +impl Parse for BoxShadow { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut lengths = None; + let mut color = None; + let mut inset = false; + + loop { + if !inset { + if input + .try_parse(|input| input.expect_ident_matching("inset")) + .is_ok() + { + inset = true; + continue; + } + } + if lengths.is_none() { + let value = input.try_parse::<_, _, ParseError>(|i| { + let horizontal = Length::parse(context, i)?; + let vertical = Length::parse(context, i)?; + let (blur, spread) = + match i.try_parse(|i| Length::parse_non_negative(context, i)) { + Ok(blur) => { + let spread = i.try_parse(|i| Length::parse(context, i)).ok(); + (Some(blur.into()), spread) + }, + Err(_) => (None, None), + }; + Ok((horizontal, vertical, blur, spread)) + }); + if let Ok(value) = value { + lengths = Some(value); + continue; + } + } + if color.is_none() { + if let Ok(value) = input.try_parse(|i| Color::parse(context, i)) { + color = Some(value); + continue; + } + } + break; + } + + let lengths = + lengths.ok_or(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))?; + Ok(BoxShadow { + base: SimpleShadow { + color: color, + horizontal: lengths.0, + vertical: lengths.1, + blur: lengths.2, + }, + spread: lengths.3, + inset: inset, + }) + } +} + +impl ToComputedValue for BoxShadow { + type ComputedValue = ComputedBoxShadow; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + ComputedBoxShadow { + base: self.base.to_computed_value(context), + spread: self + .spread + .as_ref() + .unwrap_or(&Length::zero()) + .to_computed_value(context), + inset: self.inset, + } + } + + #[inline] + fn from_computed_value(computed: &ComputedBoxShadow) -> Self { + BoxShadow { + base: ToComputedValue::from_computed_value(&computed.base), + spread: Some(ToComputedValue::from_computed_value(&computed.spread)), + inset: computed.inset, + } + } +} + +impl Parse for Filter { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + #[cfg(feature = "gecko")] + { + if let Ok(url) = input.try_parse(|i| SpecifiedUrl::parse(context, i)) { + return Ok(GenericFilter::Url(url)); + } + } + let location = input.current_source_location(); + let function = match input.expect_function() { + Ok(f) => f.clone(), + Err(cssparser::BasicParseError { + kind: BasicParseErrorKind::UnexpectedToken(t), + location, + }) => return Err(location.new_custom_error(ValueParseErrorKind::InvalidFilter(t))), + Err(e) => return Err(e.into()), + }; + input.parse_nested_block(|i| { + match_ignore_ascii_case! { &*function, + "blur" => Ok(GenericFilter::Blur( + i.try_parse(|i| NonNegativeLength::parse(context, i)) + .unwrap_or(Zero::zero()), + )), + "brightness" => Ok(GenericFilter::Brightness( + i.try_parse(|i| NonNegativeFactor::parse(context, i)) + .unwrap_or(NonNegativeFactor::one()), + )), + "contrast" => Ok(GenericFilter::Contrast( + i.try_parse(|i| NonNegativeFactor::parse(context, i)) + .unwrap_or(NonNegativeFactor::one()), + )), + "grayscale" => { + // Values of amount over 100% are allowed but UAs must clamp the values to 1. + // https://drafts.fxtf.org/filter-effects/#funcdef-filter-grayscale + Ok(GenericFilter::Grayscale( + i.try_parse(|i| ZeroToOneFactor::parse(context, i)) + .unwrap_or(ZeroToOneFactor::one()), + )) + }, + "hue-rotate" => { + // We allow unitless zero here, see: + // https://github.com/w3c/fxtf-drafts/issues/228 + Ok(GenericFilter::HueRotate( + i.try_parse(|i| Angle::parse_with_unitless(context, i)) + .unwrap_or(Zero::zero()), + )) + }, + "invert" => { + // Values of amount over 100% are allowed but UAs must clamp the values to 1. + // https://drafts.fxtf.org/filter-effects/#funcdef-filter-invert + Ok(GenericFilter::Invert( + i.try_parse(|i| ZeroToOneFactor::parse(context, i)) + .unwrap_or(ZeroToOneFactor::one()), + )) + }, + "opacity" => { + // Values of amount over 100% are allowed but UAs must clamp the values to 1. + // https://drafts.fxtf.org/filter-effects/#funcdef-filter-opacity + Ok(GenericFilter::Opacity( + i.try_parse(|i| ZeroToOneFactor::parse(context, i)) + .unwrap_or(ZeroToOneFactor::one()), + )) + }, + "saturate" => Ok(GenericFilter::Saturate( + i.try_parse(|i| NonNegativeFactor::parse(context, i)) + .unwrap_or(NonNegativeFactor::one()), + )), + "sepia" => { + // Values of amount over 100% are allowed but UAs must clamp the values to 1. + // https://drafts.fxtf.org/filter-effects/#funcdef-filter-sepia + Ok(GenericFilter::Sepia( + i.try_parse(|i| ZeroToOneFactor::parse(context, i)) + .unwrap_or(ZeroToOneFactor::one()), + )) + }, + "drop-shadow" => Ok(GenericFilter::DropShadow(Parse::parse(context, i)?)), + _ => Err(location.new_custom_error( + ValueParseErrorKind::InvalidFilter(Token::Function(function.clone())) + )), + } + }) + } +} + +impl Parse for SimpleShadow { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let color = input.try_parse(|i| Color::parse(context, i)).ok(); + let horizontal = Length::parse(context, input)?; + let vertical = Length::parse(context, input)?; + let blur = input + .try_parse(|i| Length::parse_non_negative(context, i)) + .ok(); + let blur = blur.map(NonNegative::<Length>); + let color = color.or_else(|| input.try_parse(|i| Color::parse(context, i)).ok()); + + Ok(SimpleShadow { + color, + horizontal, + vertical, + blur, + }) + } +} + +impl ToComputedValue for SimpleShadow { + type ComputedValue = ComputedSimpleShadow; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + ComputedSimpleShadow { + color: self + .color + .as_ref() + .unwrap_or(&Color::currentcolor()) + .to_computed_value(context), + horizontal: self.horizontal.to_computed_value(context), + vertical: self.vertical.to_computed_value(context), + blur: self + .blur + .as_ref() + .unwrap_or(&NonNegativeLength::zero()) + .to_computed_value(context), + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + SimpleShadow { + color: Some(ToComputedValue::from_computed_value(&computed.color)), + horizontal: ToComputedValue::from_computed_value(&computed.horizontal), + vertical: ToComputedValue::from_computed_value(&computed.vertical), + blur: Some(ToComputedValue::from_computed_value(&computed.blur)), + } + } +} diff --git a/servo/components/style/values/specified/flex.rs b/servo/components/style/values/specified/flex.rs new file mode 100644 index 0000000000..7c767cdf34 --- /dev/null +++ b/servo/components/style/values/specified/flex.rs @@ -0,0 +1,25 @@ +/* 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/. */ + +//! Specified types for CSS values related to flexbox. + +use crate::values::generics::flex::FlexBasis as GenericFlexBasis; +use crate::values::specified::Size; + +/// A specified value for the `flex-basis` property. +pub type FlexBasis = GenericFlexBasis<Size>; + +impl FlexBasis { + /// `auto` + #[inline] + pub fn auto() -> Self { + GenericFlexBasis::Size(Size::auto()) + } + + /// `0%` + #[inline] + pub fn zero_percent() -> Self { + GenericFlexBasis::Size(Size::zero_percent()) + } +} diff --git a/servo/components/style/values/specified/font.rs b/servo/components/style/values/specified/font.rs new file mode 100644 index 0000000000..fb59a53cef --- /dev/null +++ b/servo/components/style/values/specified/font.rs @@ -0,0 +1,2385 @@ +/* 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/. */ + +//! Specified values for font properties + +#[cfg(feature = "gecko")] +use crate::gecko_bindings::bindings; +use crate::parser::{Parse, ParserContext}; +use crate::properties::longhands::system_font::SystemFont; +use crate::values::computed::font::{FamilyName, FontFamilyList, FontStyleAngle, SingleFontFamily}; +use crate::values::computed::{font as computed, Length, NonNegativeLength}; +use crate::values::computed::{Angle as ComputedAngle, Percentage as ComputedPercentage}; +use crate::values::computed::{CSSPixelLength, Context, ToComputedValue}; +use crate::values::generics::font::VariationValue; +use crate::values::generics::font::{self as generics, FeatureTagValue, FontSettings, FontTag}; +use crate::values::generics::NonNegative; +use crate::values::specified::length::{FontBaseSize, AU_PER_PT, AU_PER_PX}; +use crate::values::specified::{AllowQuirks, Angle, Integer, LengthPercentage}; +use crate::values::specified::{NoCalcLength, NonNegativeNumber, Number, Percentage}; +use crate::values::CustomIdent; +use crate::Atom; +use cssparser::{Parser, Token}; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; +use std::fmt::{self, Write}; +use style_traits::values::SequenceWriter; +use style_traits::{CssWriter, KeywordsCollectFn, ParseError}; +use style_traits::{SpecifiedValueInfo, StyleParseErrorKind, ToCss}; + +// FIXME(emilio): The system font code is copy-pasta, and should be cleaned up. +macro_rules! system_font_methods { + ($ty:ident, $field:ident) => { + system_font_methods!($ty); + + fn compute_system(&self, _context: &Context) -> <$ty as ToComputedValue>::ComputedValue { + debug_assert!(matches!(*self, $ty::System(..))); + #[cfg(feature = "gecko")] + { + _context.cached_system_font.as_ref().unwrap().$field.clone() + } + #[cfg(feature = "servo")] + { + unreachable!() + } + } + }; + + ($ty:ident) => { + /// Get a specified value that represents a system font. + pub fn system_font(f: SystemFont) -> Self { + $ty::System(f) + } + + /// Retreive a SystemFont from the specified value. + pub fn get_system(&self) -> Option<SystemFont> { + if let $ty::System(s) = *self { + Some(s) + } else { + None + } + } + }; +} + +const DEFAULT_SCRIPT_MIN_SIZE_PT: u32 = 8; +const DEFAULT_SCRIPT_SIZE_MULTIPLIER: f64 = 0.71; + +/// The minimum font-weight value per: +/// +/// https://drafts.csswg.org/css-fonts-4/#font-weight-numeric-values +pub const MIN_FONT_WEIGHT: f32 = 1.; + +/// The maximum font-weight value per: +/// +/// https://drafts.csswg.org/css-fonts-4/#font-weight-numeric-values +pub const MAX_FONT_WEIGHT: f32 = 1000.; + +/// A specified font-weight value. +/// +/// https://drafts.csswg.org/css-fonts-4/#propdef-font-weight +#[derive(Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum FontWeight { + /// `<font-weight-absolute>` + Absolute(AbsoluteFontWeight), + /// Bolder variant + Bolder, + /// Lighter variant + Lighter, + /// System font variant. + #[css(skip)] + System(SystemFont), +} + +impl FontWeight { + system_font_methods!(FontWeight, font_weight); + + /// `normal` + #[inline] + pub fn normal() -> Self { + FontWeight::Absolute(AbsoluteFontWeight::Normal) + } + + /// Get a specified FontWeight from a gecko keyword + pub fn from_gecko_keyword(kw: u32) -> Self { + debug_assert!(kw % 100 == 0); + debug_assert!(kw as f32 <= MAX_FONT_WEIGHT); + FontWeight::Absolute(AbsoluteFontWeight::Weight(Number::new(kw as f32))) + } +} + +impl ToComputedValue for FontWeight { + type ComputedValue = computed::FontWeight; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + FontWeight::Absolute(ref abs) => abs.compute(), + FontWeight::Bolder => context + .builder + .get_parent_font() + .clone_font_weight() + .bolder(), + FontWeight::Lighter => context + .builder + .get_parent_font() + .clone_font_weight() + .lighter(), + FontWeight::System(_) => self.compute_system(context), + } + } + + #[inline] + fn from_computed_value(computed: &computed::FontWeight) -> Self { + FontWeight::Absolute(AbsoluteFontWeight::Weight(Number::from_computed_value( + &computed.0, + ))) + } +} + +/// An absolute font-weight value for a @font-face rule. +/// +/// https://drafts.csswg.org/css-fonts-4/#font-weight-absolute-values +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum AbsoluteFontWeight { + /// A `<number>`, with the additional constraints specified in: + /// + /// https://drafts.csswg.org/css-fonts-4/#font-weight-numeric-values + Weight(Number), + /// Normal font weight. Same as 400. + Normal, + /// Bold font weight. Same as 700. + Bold, +} + +impl AbsoluteFontWeight { + /// Returns the computed value for this absolute font weight. + pub fn compute(&self) -> computed::FontWeight { + match *self { + AbsoluteFontWeight::Weight(weight) => { + computed::FontWeight(weight.get().max(MIN_FONT_WEIGHT).min(MAX_FONT_WEIGHT)) + }, + AbsoluteFontWeight::Normal => computed::FontWeight::normal(), + AbsoluteFontWeight::Bold => computed::FontWeight::bold(), + } + } +} + +impl Parse for AbsoluteFontWeight { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(number) = input.try_parse(|input| Number::parse(context, input)) { + // We could add another AllowedNumericType value, but it doesn't + // seem worth it just for a single property with such a weird range, + // so we do the clamping here manually. + if !number.was_calc() && + (number.get() < MIN_FONT_WEIGHT || number.get() > MAX_FONT_WEIGHT) + { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + return Ok(AbsoluteFontWeight::Weight(number)); + } + + Ok(try_match_ident_ignore_ascii_case! { input, + "normal" => AbsoluteFontWeight::Normal, + "bold" => AbsoluteFontWeight::Bold, + }) + } +} + +/// The specified value of the `font-style` property, without the system font +/// crap. +pub type SpecifiedFontStyle = generics::FontStyle<Angle>; + +impl ToCss for SpecifiedFontStyle { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + generics::FontStyle::Normal => dest.write_str("normal"), + generics::FontStyle::Italic => dest.write_str("italic"), + generics::FontStyle::Oblique(ref angle) => { + dest.write_str("oblique")?; + if *angle != Self::default_angle() { + dest.write_char(' ')?; + angle.to_css(dest)?; + } + Ok(()) + }, + } + } +} + +impl Parse for SpecifiedFontStyle { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(try_match_ident_ignore_ascii_case! { input, + "normal" => generics::FontStyle::Normal, + "italic" => generics::FontStyle::Italic, + "oblique" => { + let angle = input.try_parse(|input| Self::parse_angle(context, input)) + .unwrap_or_else(|_| Self::default_angle()); + + generics::FontStyle::Oblique(angle) + }, + }) + } +} + +impl ToComputedValue for SpecifiedFontStyle { + type ComputedValue = computed::FontStyle; + + fn to_computed_value(&self, _: &Context) -> Self::ComputedValue { + match *self { + generics::FontStyle::Normal => generics::FontStyle::Normal, + generics::FontStyle::Italic => generics::FontStyle::Italic, + generics::FontStyle::Oblique(ref angle) => { + generics::FontStyle::Oblique(FontStyleAngle(Self::compute_angle(angle))) + }, + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + match *computed { + generics::FontStyle::Normal => generics::FontStyle::Normal, + generics::FontStyle::Italic => generics::FontStyle::Italic, + generics::FontStyle::Oblique(ref angle) => { + generics::FontStyle::Oblique(Angle::from_computed_value(&angle.0)) + }, + } + } +} + +/// The default angle for `font-style: oblique`. +/// +/// NOTE(emilio): As of right now this diverges from the spec, which specifies +/// 20, because it's not updated yet to account for the resolution in: +/// +/// https://github.com/w3c/csswg-drafts/issues/2295 +pub const DEFAULT_FONT_STYLE_OBLIQUE_ANGLE_DEGREES: f32 = 14.; + +/// From https://drafts.csswg.org/css-fonts-4/#valdef-font-style-oblique-angle: +/// +/// Values less than -90deg or values greater than 90deg are +/// invalid and are treated as parse errors. +/// +/// The maximum angle value that `font-style: oblique` should compute to. +pub const FONT_STYLE_OBLIQUE_MAX_ANGLE_DEGREES: f32 = 90.; + +/// The minimum angle value that `font-style: oblique` should compute to. +pub const FONT_STYLE_OBLIQUE_MIN_ANGLE_DEGREES: f32 = -90.; + +impl SpecifiedFontStyle { + /// Gets a clamped angle in degrees from a specified Angle. + pub fn compute_angle_degrees(angle: &Angle) -> f32 { + angle + .degrees() + .max(FONT_STYLE_OBLIQUE_MIN_ANGLE_DEGREES) + .min(FONT_STYLE_OBLIQUE_MAX_ANGLE_DEGREES) + } + + fn compute_angle(angle: &Angle) -> ComputedAngle { + ComputedAngle::from_degrees(Self::compute_angle_degrees(angle)) + } + + /// Parse a suitable angle for font-style: oblique. + pub fn parse_angle<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Angle, ParseError<'i>> { + let angle = Angle::parse(context, input)?; + if angle.was_calc() { + return Ok(angle); + } + + let degrees = angle.degrees(); + if degrees < FONT_STYLE_OBLIQUE_MIN_ANGLE_DEGREES || + degrees > FONT_STYLE_OBLIQUE_MAX_ANGLE_DEGREES + { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + return Ok(angle); + } + + /// The default angle for `font-style: oblique`. + pub fn default_angle() -> Angle { + Angle::from_degrees( + DEFAULT_FONT_STYLE_OBLIQUE_ANGLE_DEGREES, + /* was_calc = */ false, + ) + } +} + +/// The specified value of the `font-style` property. +#[derive(Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +#[allow(missing_docs)] +pub enum FontStyle { + Specified(SpecifiedFontStyle), + #[css(skip)] + System(SystemFont), +} + +impl FontStyle { + /// Return the `normal` value. + #[inline] + pub fn normal() -> Self { + FontStyle::Specified(generics::FontStyle::Normal) + } + + system_font_methods!(FontStyle, font_style); +} + +impl ToComputedValue for FontStyle { + type ComputedValue = computed::FontStyle; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + FontStyle::Specified(ref specified) => specified.to_computed_value(context), + FontStyle::System(..) => self.compute_system(context), + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + FontStyle::Specified(SpecifiedFontStyle::from_computed_value(computed)) + } +} + +/// A value for the `font-stretch` property. +/// +/// https://drafts.csswg.org/css-fonts-4/#font-stretch-prop +/// +/// TODO(emilio): We could derive Parse if we had NonNegativePercentage. +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +#[repr(u8)] +pub enum FontStretch { + Stretch(Percentage), + Keyword(FontStretchKeyword), + #[css(skip)] + System(SystemFont), +} + +/// A keyword value for `font-stretch`. +#[derive(Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +#[allow(missing_docs)] +pub enum FontStretchKeyword { + Normal, + Condensed, + UltraCondensed, + ExtraCondensed, + SemiCondensed, + SemiExpanded, + Expanded, + ExtraExpanded, + UltraExpanded, +} + +impl FontStretchKeyword { + /// Resolves the value of the keyword as specified in: + /// + /// https://drafts.csswg.org/css-fonts-4/#font-stretch-prop + pub fn compute(&self) -> ComputedPercentage { + use self::FontStretchKeyword::*; + ComputedPercentage(match *self { + UltraCondensed => 0.5, + ExtraCondensed => 0.625, + Condensed => 0.75, + SemiCondensed => 0.875, + Normal => 1., + SemiExpanded => 1.125, + Expanded => 1.25, + ExtraExpanded => 1.5, + UltraExpanded => 2., + }) + } + + /// Does the opposite operation to `compute`, in order to serialize keywords + /// if possible. + pub fn from_percentage(percentage: f32) -> Option<Self> { + use self::FontStretchKeyword::*; + // NOTE(emilio): Can't use `match` because of rust-lang/rust#41620. + if percentage == 0.5 { + return Some(UltraCondensed); + } + if percentage == 0.625 { + return Some(ExtraCondensed); + } + if percentage == 0.75 { + return Some(Condensed); + } + if percentage == 0.875 { + return Some(SemiCondensed); + } + if percentage == 1. { + return Some(Normal); + } + if percentage == 1.125 { + return Some(SemiExpanded); + } + if percentage == 1.25 { + return Some(Expanded); + } + if percentage == 1.5 { + return Some(ExtraExpanded); + } + if percentage == 2. { + return Some(UltraExpanded); + } + None + } +} + +impl FontStretch { + /// `normal`. + pub fn normal() -> Self { + FontStretch::Keyword(FontStretchKeyword::Normal) + } + + system_font_methods!(FontStretch, font_stretch); +} + +impl Parse for FontStretch { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // From https://drafts.csswg.org/css-fonts-4/#font-stretch-prop: + // + // Values less than 0% are not allowed and are treated as parse + // errors. + if let Ok(percentage) = + input.try_parse(|input| Percentage::parse_non_negative(context, input)) + { + return Ok(FontStretch::Stretch(percentage)); + } + + Ok(FontStretch::Keyword(FontStretchKeyword::parse(input)?)) + } +} + +impl ToComputedValue for FontStretch { + type ComputedValue = computed::FontStretch; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + FontStretch::Stretch(ref percentage) => { + computed::FontStretch(NonNegative(percentage.to_computed_value(context))) + }, + FontStretch::Keyword(ref kw) => computed::FontStretch(NonNegative(kw.compute())), + FontStretch::System(_) => self.compute_system(context), + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + FontStretch::Stretch(Percentage::from_computed_value(&(computed.0).0)) + } +} + +/// CSS font keywords +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, + Serialize, + Deserialize, +)] +#[allow(missing_docs)] +#[repr(u8)] +pub enum FontSizeKeyword { + #[css(keyword = "xx-small")] + XXSmall, + XSmall, + Small, + Medium, + Large, + XLarge, + #[css(keyword = "xx-large")] + XXLarge, + #[css(keyword = "xxx-large")] + XXXLarge, + #[css(skip)] + None, +} + +impl FontSizeKeyword { + /// Convert to an HTML <font size> value + #[inline] + pub fn html_size(self) -> u8 { + self as u8 + } +} + +impl Default for FontSizeKeyword { + fn default() -> Self { + FontSizeKeyword::Medium + } +} + +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[cfg_attr(feature = "servo", derive(Serialize, Deserialize))] +/// Additional information for keyword-derived font sizes. +pub struct KeywordInfo { + /// The keyword used + pub kw: FontSizeKeyword, + /// A factor to be multiplied by the computed size of the keyword + #[css(skip)] + pub factor: f32, + /// An additional fixed offset to add to the kw * factor in the case of + /// `calc()`. + #[css(skip)] + pub offset: CSSPixelLength, +} + +impl KeywordInfo { + /// KeywordInfo value for font-size: medium + pub fn medium() -> Self { + Self::new(FontSizeKeyword::Medium) + } + + /// KeywordInfo value for font-size: none + pub fn none() -> Self { + Self::new(FontSizeKeyword::None) + } + + fn new(kw: FontSizeKeyword) -> Self { + KeywordInfo { + kw, + factor: 1., + offset: CSSPixelLength::new(0.), + } + } + + /// Computes the final size for this font-size keyword, accounting for + /// text-zoom. + fn to_computed_value(&self, context: &Context) -> CSSPixelLength { + debug_assert_ne!(self.kw, FontSizeKeyword::None); + let base = context.maybe_zoom_text(self.kw.to_length(context).0); + base * self.factor + context.maybe_zoom_text(self.offset) + } + + /// Given a parent keyword info (self), apply an additional factor/offset to + /// it. + fn compose(self, factor: f32) -> Self { + if self.kw == FontSizeKeyword::None { + return self; + } + KeywordInfo { + kw: self.kw, + factor: self.factor * factor, + offset: self.offset * factor, + } + } +} + +impl SpecifiedValueInfo for KeywordInfo { + fn collect_completion_keywords(f: KeywordsCollectFn) { + <FontSizeKeyword as SpecifiedValueInfo>::collect_completion_keywords(f); + } +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +/// A specified font-size value +pub enum FontSize { + /// A length; e.g. 10px. + Length(LengthPercentage), + /// A keyword value, along with a ratio and absolute offset. + /// The ratio in any specified keyword value + /// will be 1 (with offset 0), but we cascade keywordness even + /// after font-relative (percent and em) values + /// have been applied, which is where the ratio + /// comes in. The offset comes in if we cascaded a calc value, + /// where the font-relative portion (em and percentage) will + /// go into the ratio, and the remaining units all computed together + /// will go into the offset. + /// See bug 1355707. + Keyword(KeywordInfo), + /// font-size: smaller + Smaller, + /// font-size: larger + Larger, + /// Derived from a specified system font. + #[css(skip)] + System(SystemFont), +} + +/// Specifies a prioritized list of font family names or generic family names. +#[derive(Clone, Debug, Eq, PartialEq, ToCss, ToShmem)] +#[cfg_attr(feature = "servo", derive(Hash))] +pub enum FontFamily { + /// List of `font-family` + #[css(comma)] + Values(#[css(iterable)] FontFamilyList), + /// System font + #[css(skip)] + System(SystemFont), +} + +impl FontFamily { + system_font_methods!(FontFamily, font_family); + + /// Parse a specified font-family value + pub fn parse_specified<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> { + let values = input.parse_comma_separated(SingleFontFamily::parse)?; + Ok(FontFamily::Values(FontFamilyList::new( + values.into_boxed_slice(), + ))) + } +} + +impl ToComputedValue for FontFamily { + type ComputedValue = computed::FontFamily; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + FontFamily::Values(ref v) => computed::FontFamily { + families: v.clone(), + is_system_font: false, + }, + FontFamily::System(_) => self.compute_system(context), + } + } + + fn from_computed_value(other: &computed::FontFamily) -> Self { + FontFamily::Values(other.families.clone()) + } +} + +#[cfg(feature = "gecko")] +impl MallocSizeOf for FontFamily { + fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { + match *self { + FontFamily::Values(ref v) => { + // Although a SharedFontList object is refcounted, we always + // attribute its size to the specified value, as long as it's + // not a value in SharedFontList::sSingleGenerics. + if matches!(v, FontFamilyList::SharedFontList(_)) { + let ptr = v.shared_font_list().get(); + unsafe { bindings::Gecko_SharedFontList_SizeOfIncludingThis(ptr) } + } else { + 0 + } + }, + FontFamily::System(_) => 0, + } + } +} + +impl Parse for FontFamily { + /// <family-name># + /// <family-name> = <string> | [ <ident>+ ] + /// TODO: <generic-family> + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontFamily, ParseError<'i>> { + FontFamily::parse_specified(input) + } +} + +impl SpecifiedValueInfo for FontFamily {} + +/// `FamilyName::parse` is based on `SingleFontFamily::parse` and not the other way around +/// because we want the former to exclude generic family keywords. +impl Parse for FamilyName { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + match SingleFontFamily::parse(input) { + Ok(SingleFontFamily::FamilyName(name)) => Ok(name), + Ok(SingleFontFamily::Generic(_)) => { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + }, + Err(e) => Err(e), + } + } +} + +#[derive(Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +/// Preserve the readability of text when font fallback occurs +pub enum FontSizeAdjust { + /// None variant + None, + /// Number variant + Number(NonNegativeNumber), + /// system font + #[css(skip)] + System(SystemFont), +} + +impl FontSizeAdjust { + #[inline] + /// Default value of font-size-adjust + pub fn none() -> Self { + FontSizeAdjust::None + } + + system_font_methods!(FontSizeAdjust, font_size_adjust); +} + +impl ToComputedValue for FontSizeAdjust { + type ComputedValue = computed::FontSizeAdjust; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + FontSizeAdjust::None => computed::FontSizeAdjust::None, + FontSizeAdjust::Number(ref n) => { + // The computed version handles clamping of animated values + // itself. + computed::FontSizeAdjust::Number(n.to_computed_value(context).0) + }, + FontSizeAdjust::System(_) => self.compute_system(context), + } + } + + fn from_computed_value(computed: &computed::FontSizeAdjust) -> Self { + match *computed { + computed::FontSizeAdjust::None => FontSizeAdjust::None, + computed::FontSizeAdjust::Number(v) => { + FontSizeAdjust::Number(NonNegativeNumber::from_computed_value(&v.into())) + }, + } + } +} + +/// This is the ratio applied for font-size: larger +/// and smaller by both Firefox and Chrome +const LARGER_FONT_SIZE_RATIO: f32 = 1.2; + +/// The default font size. +pub const FONT_MEDIUM_PX: f32 = 16.0; + +impl FontSizeKeyword { + #[inline] + #[cfg(feature = "servo")] + fn to_length(&self, _: &Context) -> NonNegativeLength { + let medium = Length::new(FONT_MEDIUM_PX); + // https://drafts.csswg.org/css-fonts-3/#font-size-prop + NonNegative(match *self { + FontSizeKeyword::XXSmall => medium * 3.0 / 5.0, + FontSizeKeyword::XSmall => medium * 3.0 / 4.0, + FontSizeKeyword::Small => medium * 8.0 / 9.0, + FontSizeKeyword::Medium => medium, + FontSizeKeyword::Large => medium * 6.0 / 5.0, + FontSizeKeyword::XLarge => medium * 3.0 / 2.0, + FontSizeKeyword::XXLarge => medium * 2.0, + FontSizeKeyword::XXXLarge => medium * 3.0, + FontSizeKeyword::None => unreachable!(), + }) + } + + #[cfg(feature = "gecko")] + #[inline] + fn to_length(&self, cx: &Context) -> NonNegativeLength { + use crate::context::QuirksMode; + + // The tables in this function are originally from + // nsRuleNode::CalcFontPointSize in Gecko: + // + // https://searchfox.org/mozilla-central/rev/c05d9d61188d32b8/layout/style/nsRuleNode.cpp#3150 + // + // Mapping from base size and HTML size to pixels + // The first index is (base_size - 9), the second is the + // HTML size. "0" is CSS keyword xx-small, not HTML size 0, + // since HTML size 0 is the same as 1. + // + // xxs xs s m l xl xxl - + // - 0/1 2 3 4 5 6 7 + static FONT_SIZE_MAPPING: [[i32; 8]; 8] = [ + [9, 9, 9, 9, 11, 14, 18, 27], + [9, 9, 9, 10, 12, 15, 20, 30], + [9, 9, 10, 11, 13, 17, 22, 33], + [9, 9, 10, 12, 14, 18, 24, 36], + [9, 10, 12, 13, 16, 20, 26, 39], + [9, 10, 12, 14, 17, 21, 28, 42], + [9, 10, 13, 15, 18, 23, 30, 45], + [9, 10, 13, 16, 18, 24, 32, 48], + ]; + + // This table gives us compatibility with WinNav4 for the default fonts only. + // In WinNav4, the default fonts were: + // + // Times/12pt == Times/16px at 96ppi + // Courier/10pt == Courier/13px at 96ppi + // + // xxs xs s m l xl xxl - + // - 1 2 3 4 5 6 7 + static QUIRKS_FONT_SIZE_MAPPING: [[i32; 8]; 8] = [ + [9, 9, 9, 9, 11, 14, 18, 28], + [9, 9, 9, 10, 12, 15, 20, 31], + [9, 9, 9, 11, 13, 17, 22, 34], + [9, 9, 10, 12, 14, 18, 24, 37], + [9, 9, 10, 13, 16, 20, 26, 40], + [9, 9, 11, 14, 17, 21, 28, 42], + [9, 10, 12, 15, 17, 23, 30, 45], + [9, 10, 13, 16, 18, 24, 32, 48], + ]; + + static FONT_SIZE_FACTORS: [i32; 8] = [60, 75, 89, 100, 120, 150, 200, 300]; + + let ref gecko_font = cx.style().get_font().gecko(); + let base_size = unsafe { + Atom::with(gecko_font.mLanguage.mRawPtr, |atom| { + cx.font_metrics_provider + .get_size(atom, gecko_font.mGenericID) + }) + }; + + let base_size_px = base_size.px().round() as i32; + let html_size = self.html_size() as usize; + NonNegative(if base_size_px >= 9 && base_size_px <= 16 { + let mapping = if cx.quirks_mode == QuirksMode::Quirks { + QUIRKS_FONT_SIZE_MAPPING + } else { + FONT_SIZE_MAPPING + }; + Length::new(mapping[(base_size_px - 9) as usize][html_size] as f32) + } else { + base_size * FONT_SIZE_FACTORS[html_size] as f32 / 100.0 + }) + } +} + +impl FontSize { + /// <https://html.spec.whatwg.org/multipage/#rules-for-parsing-a-legacy-font-size> + pub fn from_html_size(size: u8) -> Self { + FontSize::Keyword(KeywordInfo::new(match size { + // If value is less than 1, let it be 1. + 0 | 1 => FontSizeKeyword::XSmall, + 2 => FontSizeKeyword::Small, + 3 => FontSizeKeyword::Medium, + 4 => FontSizeKeyword::Large, + 5 => FontSizeKeyword::XLarge, + 6 => FontSizeKeyword::XXLarge, + // If value is greater than 7, let it be 7. + _ => FontSizeKeyword::XXXLarge, + })) + } + + /// Compute it against a given base font size + pub fn to_computed_value_against( + &self, + context: &Context, + base_size: FontBaseSize, + ) -> computed::FontSize { + use crate::values::specified::length::FontRelativeLength; + + let compose_keyword = |factor| { + context + .style() + .get_parent_font() + .clone_font_size() + .keyword_info + .compose(factor) + }; + let mut info = KeywordInfo::none(); + let size = match *self { + FontSize::Length(LengthPercentage::Length(NoCalcLength::FontRelative(value))) => { + if let FontRelativeLength::Em(em) = value { + // If the parent font was keyword-derived, this is too. + // Tack the em unit onto the factor + info = compose_keyword(em); + } + value.to_computed_value(context, base_size) + }, + FontSize::Length(LengthPercentage::Length(NoCalcLength::ServoCharacterWidth( + value, + ))) => value.to_computed_value(base_size.resolve(context)), + FontSize::Length(LengthPercentage::Length(NoCalcLength::Absolute(ref l))) => { + context.maybe_zoom_text(l.to_computed_value(context)) + }, + FontSize::Length(LengthPercentage::Length(ref l)) => l.to_computed_value(context), + FontSize::Length(LengthPercentage::Percentage(pc)) => { + // If the parent font was keyword-derived, this is too. + // Tack the % onto the factor + info = compose_keyword(pc.0); + (base_size.resolve(context) * pc.0).normalized() + }, + FontSize::Length(LengthPercentage::Calc(ref calc)) => { + let calc = calc.to_computed_value_zoomed(context, base_size); + calc.resolve(base_size.resolve(context)) + }, + FontSize::Keyword(i) => { + // As a specified keyword, this is keyword derived + info = i; + i.to_computed_value(context).clamp_to_non_negative() + }, + FontSize::Smaller => { + info = compose_keyword(1. / LARGER_FONT_SIZE_RATIO); + FontRelativeLength::Em(1. / LARGER_FONT_SIZE_RATIO) + .to_computed_value(context, base_size) + }, + FontSize::Larger => { + info = compose_keyword(LARGER_FONT_SIZE_RATIO); + FontRelativeLength::Em(LARGER_FONT_SIZE_RATIO).to_computed_value(context, base_size) + }, + + FontSize::System(_) => { + #[cfg(feature = "servo")] + { + unreachable!() + } + #[cfg(feature = "gecko")] + { + context + .cached_system_font + .as_ref() + .unwrap() + .font_size + .size + .0 + } + }, + }; + computed::FontSize { + size: NonNegative(size), + keyword_info: info, + } + } +} + +impl ToComputedValue for FontSize { + type ComputedValue = computed::FontSize; + + #[inline] + fn to_computed_value(&self, context: &Context) -> computed::FontSize { + self.to_computed_value_against(context, FontBaseSize::InheritedStyle) + } + + #[inline] + fn from_computed_value(computed: &computed::FontSize) -> Self { + FontSize::Length(LengthPercentage::Length( + ToComputedValue::from_computed_value(&computed.size.0), + )) + } +} + +impl FontSize { + system_font_methods!(FontSize); + + /// Get initial value for specified font size. + #[inline] + pub fn medium() -> Self { + FontSize::Keyword(KeywordInfo::medium()) + } + + /// Parses a font-size, with quirks. + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<FontSize, ParseError<'i>> { + if let Ok(lp) = input + .try_parse(|i| LengthPercentage::parse_non_negative_quirky(context, i, allow_quirks)) + { + return Ok(FontSize::Length(lp)); + } + + if let Ok(kw) = input.try_parse(FontSizeKeyword::parse) { + return Ok(FontSize::Keyword(KeywordInfo::new(kw))); + } + + try_match_ident_ignore_ascii_case! { input, + "smaller" => Ok(FontSize::Smaller), + "larger" => Ok(FontSize::Larger), + } + } +} + +impl Parse for FontSize { + /// <length> | <percentage> | <absolute-size> | <relative-size> + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontSize, ParseError<'i>> { + FontSize::parse_quirky(context, input, AllowQuirks::No) + } +} + +bitflags! { + #[cfg_attr(feature = "servo", derive(MallocSizeOf))] + /// Flags of variant alternates in bit + struct VariantAlternatesParsingFlags: u8 { + /// None of variant alternates enabled + const NORMAL = 0; + /// Historical forms + const HISTORICAL_FORMS = 0x01; + /// Stylistic Alternates + const STYLISTIC = 0x02; + /// Stylistic Sets + const STYLESET = 0x04; + /// Character Variant + const CHARACTER_VARIANT = 0x08; + /// Swash glyphs + const SWASH = 0x10; + /// Ornaments glyphs + const ORNAMENTS = 0x20; + /// Annotation forms + const ANNOTATION = 0x40; + } +} + +#[derive( + Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToResolvedValue, ToShmem, +)] +#[repr(C, u8)] +/// Set of variant alternates +pub enum VariantAlternates { + /// Enables display of stylistic alternates + #[css(function)] + Stylistic(CustomIdent), + /// Enables display with stylistic sets + #[css(comma, function)] + Styleset(#[css(iterable)] crate::OwnedSlice<CustomIdent>), + /// Enables display of specific character variants + #[css(comma, function)] + CharacterVariant(#[css(iterable)] crate::OwnedSlice<CustomIdent>), + /// Enables display of swash glyphs + #[css(function)] + Swash(CustomIdent), + /// Enables replacement of default glyphs with ornaments + #[css(function)] + Ornaments(CustomIdent), + /// Enables display of alternate annotation forms + #[css(function)] + Annotation(CustomIdent), + /// Enables display of historical forms + HistoricalForms, +} + +#[derive( + Clone, + Debug, + Default, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +/// List of Variant Alternates +pub struct VariantAlternatesList( + #[css(if_empty = "normal", iterable)] crate::OwnedSlice<VariantAlternates>, +); + +impl VariantAlternatesList { + /// Returns the length of all variant alternates. + pub fn len(&self) -> usize { + self.0.iter().fold(0, |acc, alternate| match *alternate { + VariantAlternates::Swash(_) | + VariantAlternates::Stylistic(_) | + VariantAlternates::Ornaments(_) | + VariantAlternates::Annotation(_) => acc + 1, + VariantAlternates::Styleset(ref slice) | + VariantAlternates::CharacterVariant(ref slice) => acc + slice.len(), + _ => acc, + }) + } +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +/// Control over the selection of these alternate glyphs +pub enum FontVariantAlternates { + /// Use alternative glyph from value + Value(VariantAlternatesList), + /// Use system font glyph + #[css(skip)] + System(SystemFont), +} + +impl FontVariantAlternates { + #[inline] + /// Get initial specified value with VariantAlternatesList + pub fn get_initial_specified_value() -> Self { + FontVariantAlternates::Value(Default::default()) + } + + system_font_methods!(FontVariantAlternates, font_variant_alternates); +} + +impl ToComputedValue for FontVariantAlternates { + type ComputedValue = computed::FontVariantAlternates; + + fn to_computed_value(&self, context: &Context) -> computed::FontVariantAlternates { + match *self { + FontVariantAlternates::Value(ref v) => v.clone(), + FontVariantAlternates::System(_) => self.compute_system(context), + } + } + + fn from_computed_value(other: &computed::FontVariantAlternates) -> Self { + FontVariantAlternates::Value(other.clone()) + } +} + +impl Parse for FontVariantAlternates { + /// normal | + /// [ stylistic(<feature-value-name>) || + /// historical-forms || + /// styleset(<feature-value-name> #) || + /// character-variant(<feature-value-name> #) || + /// swash(<feature-value-name>) || + /// ornaments(<feature-value-name>) || + /// annotation(<feature-value-name>) ] + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontVariantAlternates, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("normal")) + .is_ok() + { + return Ok(FontVariantAlternates::Value(Default::default())); + } + + let mut alternates = Vec::new(); + let mut parsed_alternates = VariantAlternatesParsingFlags::empty(); + macro_rules! check_if_parsed( + ($input:expr, $flag:path) => ( + if parsed_alternates.contains($flag) { + return Err($input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + parsed_alternates |= $flag; + ) + ); + while let Ok(_) = input.try_parse(|input| match *input.next()? { + Token::Ident(ref value) if value.eq_ignore_ascii_case("historical-forms") => { + check_if_parsed!(input, VariantAlternatesParsingFlags::HISTORICAL_FORMS); + alternates.push(VariantAlternates::HistoricalForms); + Ok(()) + }, + Token::Function(ref name) => { + let name = name.clone(); + input.parse_nested_block(|i| { + match_ignore_ascii_case! { &name, + "swash" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::SWASH); + let location = i.current_source_location(); + let ident = CustomIdent::from_ident(location, i.expect_ident()?, &[])?; + alternates.push(VariantAlternates::Swash(ident)); + Ok(()) + }, + "stylistic" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::STYLISTIC); + let location = i.current_source_location(); + let ident = CustomIdent::from_ident(location, i.expect_ident()?, &[])?; + alternates.push(VariantAlternates::Stylistic(ident)); + Ok(()) + }, + "ornaments" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::ORNAMENTS); + let location = i.current_source_location(); + let ident = CustomIdent::from_ident(location, i.expect_ident()?, &[])?; + alternates.push(VariantAlternates::Ornaments(ident)); + Ok(()) + }, + "annotation" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::ANNOTATION); + let location = i.current_source_location(); + let ident = CustomIdent::from_ident(location, i.expect_ident()?, &[])?; + alternates.push(VariantAlternates::Annotation(ident)); + Ok(()) + }, + "styleset" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::STYLESET); + let idents = i.parse_comma_separated(|i| { + let location = i.current_source_location(); + CustomIdent::from_ident(location, i.expect_ident()?, &[]) + })?; + alternates.push(VariantAlternates::Styleset(idents.into())); + Ok(()) + }, + "character-variant" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::CHARACTER_VARIANT); + let idents = i.parse_comma_separated(|i| { + let location = i.current_source_location(); + CustomIdent::from_ident(location, i.expect_ident()?, &[]) + })?; + alternates.push(VariantAlternates::CharacterVariant(idents.into())); + Ok(()) + }, + _ => return Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } + }) + }, + _ => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + }) {} + + if parsed_alternates.is_empty() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(FontVariantAlternates::Value(VariantAlternatesList( + alternates.into(), + ))) + } +} + +macro_rules! impl_variant_east_asian { + { + $( + $(#[$($meta:tt)+])* + $ident:ident / $css:expr => $gecko:ident = $value:expr, + )+ + } => { + bitflags! { + #[derive(MallocSizeOf, ToResolvedValue, ToShmem)] + /// Vairants for east asian variant + pub struct VariantEastAsian: u16 { + /// None of the features + const NORMAL = 0; + $( + $(#[$($meta)+])* + const $ident = $value; + )+ + } + } + + impl ToCss for VariantEastAsian { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.is_empty() { + return dest.write_str("normal"); + } + + let mut writer = SequenceWriter::new(dest, " "); + $( + if self.intersects(VariantEastAsian::$ident) { + writer.raw_item($css)?; + } + )+ + Ok(()) + } + } + + /// Asserts that all variant-east-asian matches its NS_FONT_VARIANT_EAST_ASIAN_* value. + #[cfg(feature = "gecko")] + #[inline] + pub fn assert_variant_east_asian_matches() { + use crate::gecko_bindings::structs; + $( + debug_assert_eq!(structs::$gecko as u16, VariantEastAsian::$ident.bits()); + )+ + } + + impl SpecifiedValueInfo for VariantEastAsian { + fn collect_completion_keywords(f: KeywordsCollectFn) { + f(&["normal", $($css,)+]); + } + } + } +} + +impl_variant_east_asian! { + /// Enables rendering of JIS78 forms (OpenType feature: jp78) + JIS78 / "jis78" => NS_FONT_VARIANT_EAST_ASIAN_JIS78 = 0x01, + /// Enables rendering of JIS83 forms (OpenType feature: jp83). + JIS83 / "jis83" => NS_FONT_VARIANT_EAST_ASIAN_JIS83 = 0x02, + /// Enables rendering of JIS90 forms (OpenType feature: jp90). + JIS90 / "jis90" => NS_FONT_VARIANT_EAST_ASIAN_JIS90 = 0x04, + /// Enables rendering of JIS2004 forms (OpenType feature: jp04). + JIS04 / "jis04" => NS_FONT_VARIANT_EAST_ASIAN_JIS04 = 0x08, + /// Enables rendering of simplified forms (OpenType feature: smpl). + SIMPLIFIED / "simplified" => NS_FONT_VARIANT_EAST_ASIAN_SIMPLIFIED = 0x10, + /// Enables rendering of traditional forms (OpenType feature: trad). + TRADITIONAL / "traditional" => NS_FONT_VARIANT_EAST_ASIAN_TRADITIONAL = 0x20, + /// Enables rendering of full-width variants (OpenType feature: fwid). + FULL_WIDTH / "full-width" => NS_FONT_VARIANT_EAST_ASIAN_FULL_WIDTH = 0x40, + /// Enables rendering of proportionally-spaced variants (OpenType feature: pwid). + PROPORTIONAL_WIDTH / "proportional-width" => NS_FONT_VARIANT_EAST_ASIAN_PROP_WIDTH = 0x80, + /// Enables display of ruby variant glyphs (OpenType feature: ruby). + RUBY / "ruby" => NS_FONT_VARIANT_EAST_ASIAN_RUBY = 0x100, +} + +#[cfg(feature = "gecko")] +impl VariantEastAsian { + /// Obtain a specified value from a Gecko keyword value + /// + /// Intended for use with presentation attributes, not style structs + pub fn from_gecko_keyword(kw: u16) -> Self { + Self::from_bits_truncate(kw) + } + + /// Transform into gecko keyword + pub fn to_gecko_keyword(self) -> u16 { + self.bits() + } +} + +#[cfg(feature = "gecko")] +impl_gecko_keyword_conversions!(VariantEastAsian, u16); + +#[cfg_attr(feature = "gecko", derive(MallocSizeOf))] +#[derive(Clone, Copy, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +/// Allows control of glyph substitution and sizing in East Asian text. +pub enum FontVariantEastAsian { + /// Value variant with `variant-east-asian` + Value(VariantEastAsian), + /// System font variant + #[css(skip)] + System(SystemFont), +} + +impl FontVariantEastAsian { + #[inline] + /// Get default `font-variant-east-asian` with `empty` variant + pub fn empty() -> Self { + FontVariantEastAsian::Value(VariantEastAsian::empty()) + } + + system_font_methods!(FontVariantEastAsian, font_variant_east_asian); +} + +impl ToComputedValue for FontVariantEastAsian { + type ComputedValue = computed::FontVariantEastAsian; + + fn to_computed_value(&self, context: &Context) -> computed::FontVariantEastAsian { + match *self { + FontVariantEastAsian::Value(ref v) => v.clone(), + FontVariantEastAsian::System(_) => self.compute_system(context), + } + } + + fn from_computed_value(other: &computed::FontVariantEastAsian) -> Self { + FontVariantEastAsian::Value(other.clone()) + } +} + +impl Parse for FontVariantEastAsian { + /// normal | [ <east-asian-variant-values> || <east-asian-width-values> || ruby ] + /// <east-asian-variant-values> = [ jis78 | jis83 | jis90 | jis04 | simplified | traditional ] + /// <east-asian-width-values> = [ full-width | proportional-width ] + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontVariantEastAsian, ParseError<'i>> { + let mut result = VariantEastAsian::empty(); + + if input + .try_parse(|input| input.expect_ident_matching("normal")) + .is_ok() + { + return Ok(FontVariantEastAsian::Value(result)); + } + + while let Ok(flag) = input.try_parse(|input| { + Ok( + match_ignore_ascii_case! { &input.expect_ident().map_err(|_| ())?, + "jis78" => + exclusive_value!((result, VariantEastAsian::JIS78 | VariantEastAsian::JIS83 | + VariantEastAsian::JIS90 | VariantEastAsian::JIS04 | + VariantEastAsian::SIMPLIFIED | VariantEastAsian::TRADITIONAL + ) => VariantEastAsian::JIS78), + "jis83" => + exclusive_value!((result, VariantEastAsian::JIS78 | VariantEastAsian::JIS83 | + VariantEastAsian::JIS90 | VariantEastAsian::JIS04 | + VariantEastAsian::SIMPLIFIED | VariantEastAsian::TRADITIONAL + ) => VariantEastAsian::JIS83), + "jis90" => + exclusive_value!((result, VariantEastAsian::JIS78 | VariantEastAsian::JIS83 | + VariantEastAsian::JIS90 | VariantEastAsian::JIS04 | + VariantEastAsian::SIMPLIFIED | VariantEastAsian::TRADITIONAL + ) => VariantEastAsian::JIS90), + "jis04" => + exclusive_value!((result, VariantEastAsian::JIS78 | VariantEastAsian::JIS83 | + VariantEastAsian::JIS90 | VariantEastAsian::JIS04 | + VariantEastAsian::SIMPLIFIED | VariantEastAsian::TRADITIONAL + ) => VariantEastAsian::JIS04), + "simplified" => + exclusive_value!((result, VariantEastAsian::JIS78 | VariantEastAsian::JIS83 | + VariantEastAsian::JIS90 | VariantEastAsian::JIS04 | + VariantEastAsian::SIMPLIFIED | VariantEastAsian::TRADITIONAL + ) => VariantEastAsian::SIMPLIFIED), + "traditional" => + exclusive_value!((result, VariantEastAsian::JIS78 | VariantEastAsian::JIS83 | + VariantEastAsian::JIS90 | VariantEastAsian::JIS04 | + VariantEastAsian::SIMPLIFIED | VariantEastAsian::TRADITIONAL + ) => VariantEastAsian::TRADITIONAL), + "full-width" => + exclusive_value!((result, VariantEastAsian::FULL_WIDTH | + VariantEastAsian::PROPORTIONAL_WIDTH + ) => VariantEastAsian::FULL_WIDTH), + "proportional-width" => + exclusive_value!((result, VariantEastAsian::FULL_WIDTH | + VariantEastAsian::PROPORTIONAL_WIDTH + ) => VariantEastAsian::PROPORTIONAL_WIDTH), + "ruby" => + exclusive_value!((result, VariantEastAsian::RUBY) => VariantEastAsian::RUBY), + _ => return Err(()), + }, + ) + }) { + result.insert(flag); + } + + if !result.is_empty() { + Ok(FontVariantEastAsian::Value(result)) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +macro_rules! impl_variant_ligatures { + { + $( + $(#[$($meta:tt)+])* + $ident:ident / $css:expr => $gecko:ident = $value:expr, + )+ + } => { + bitflags! { + #[derive(MallocSizeOf, ToResolvedValue, ToShmem)] + /// Variants of ligatures + pub struct VariantLigatures: u16 { + /// Specifies that common default features are enabled + const NORMAL = 0; + $( + $(#[$($meta)+])* + const $ident = $value; + )+ + } + } + + impl ToCss for VariantLigatures { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.is_empty() { + return dest.write_str("normal"); + } + if self.contains(VariantLigatures::NONE) { + return dest.write_str("none"); + } + + let mut writer = SequenceWriter::new(dest, " "); + $( + if self.intersects(VariantLigatures::$ident) { + writer.raw_item($css)?; + } + )+ + Ok(()) + } + } + + /// Asserts that all variant-east-asian matches its NS_FONT_VARIANT_EAST_ASIAN_* value. + #[cfg(feature = "gecko")] + #[inline] + pub fn assert_variant_ligatures_matches() { + use crate::gecko_bindings::structs; + $( + debug_assert_eq!(structs::$gecko as u16, VariantLigatures::$ident.bits()); + )+ + } + + impl SpecifiedValueInfo for VariantLigatures { + fn collect_completion_keywords(f: KeywordsCollectFn) { + f(&["normal", $($css,)+]); + } + } + } +} + +impl_variant_ligatures! { + /// Specifies that all types of ligatures and contextual forms + /// covered by this property are explicitly disabled + NONE / "none" => NS_FONT_VARIANT_LIGATURES_NONE = 0x01, + /// Enables display of common ligatures + COMMON_LIGATURES / "common-ligatures" => NS_FONT_VARIANT_LIGATURES_COMMON = 0x02, + /// Disables display of common ligatures + NO_COMMON_LIGATURES / "no-common-ligatures" => NS_FONT_VARIANT_LIGATURES_NO_COMMON = 0x04, + /// Enables display of discretionary ligatures + DISCRETIONARY_LIGATURES / "discretionary-ligatures" => NS_FONT_VARIANT_LIGATURES_DISCRETIONARY = 0x08, + /// Disables display of discretionary ligatures + NO_DISCRETIONARY_LIGATURES / "no-discretionary-ligatures" => NS_FONT_VARIANT_LIGATURES_NO_DISCRETIONARY = 0x10, + /// Enables display of historical ligatures + HISTORICAL_LIGATURES / "historical-ligatures" => NS_FONT_VARIANT_LIGATURES_HISTORICAL = 0x20, + /// Disables display of historical ligatures + NO_HISTORICAL_LIGATURES / "no-historical-ligatures" => NS_FONT_VARIANT_LIGATURES_NO_HISTORICAL = 0x40, + /// Enables display of contextual alternates + CONTEXTUAL / "contextual" => NS_FONT_VARIANT_LIGATURES_CONTEXTUAL = 0x80, + /// Disables display of contextual alternates + NO_CONTEXTUAL / "no-contextual" => NS_FONT_VARIANT_LIGATURES_NO_CONTEXTUAL = 0x100, +} + +#[cfg(feature = "gecko")] +impl VariantLigatures { + /// Obtain a specified value from a Gecko keyword value + /// + /// Intended for use with presentation attributes, not style structs + pub fn from_gecko_keyword(kw: u16) -> Self { + Self::from_bits_truncate(kw) + } + + /// Transform into gecko keyword + pub fn to_gecko_keyword(self) -> u16 { + self.bits() + } +} + +#[cfg(feature = "gecko")] +impl_gecko_keyword_conversions!(VariantLigatures, u16); + +#[cfg_attr(feature = "gecko", derive(MallocSizeOf))] +#[derive(Clone, Copy, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +/// Ligatures and contextual forms are ways of combining glyphs +/// to produce more harmonized forms +pub enum FontVariantLigatures { + /// Value variant with `variant-ligatures` + Value(VariantLigatures), + /// System font variant + #[css(skip)] + System(SystemFont), +} + +impl FontVariantLigatures { + system_font_methods!(FontVariantLigatures, font_variant_ligatures); + + /// Default value of `font-variant-ligatures` as `empty` + #[inline] + pub fn empty() -> FontVariantLigatures { + FontVariantLigatures::Value(VariantLigatures::empty()) + } + + #[inline] + /// Get `none` variant of `font-variant-ligatures` + pub fn none() -> FontVariantLigatures { + FontVariantLigatures::Value(VariantLigatures::NONE) + } +} + +impl ToComputedValue for FontVariantLigatures { + type ComputedValue = computed::FontVariantLigatures; + + fn to_computed_value(&self, context: &Context) -> computed::FontVariantLigatures { + match *self { + FontVariantLigatures::Value(ref v) => v.clone(), + FontVariantLigatures::System(_) => self.compute_system(context), + } + } + + fn from_computed_value(other: &computed::FontVariantLigatures) -> Self { + FontVariantLigatures::Value(other.clone()) + } +} + +impl Parse for FontVariantLigatures { + /// normal | none | + /// [ <common-lig-values> || + /// <discretionary-lig-values> || + /// <historical-lig-values> || + /// <contextual-alt-values> ] + /// <common-lig-values> = [ common-ligatures | no-common-ligatures ] + /// <discretionary-lig-values> = [ discretionary-ligatures | no-discretionary-ligatures ] + /// <historical-lig-values> = [ historical-ligatures | no-historical-ligatures ] + /// <contextual-alt-values> = [ contextual | no-contextual ] + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontVariantLigatures, ParseError<'i>> { + let mut result = VariantLigatures::empty(); + + if input + .try_parse(|input| input.expect_ident_matching("normal")) + .is_ok() + { + return Ok(FontVariantLigatures::Value(result)); + } + if input + .try_parse(|input| input.expect_ident_matching("none")) + .is_ok() + { + return Ok(FontVariantLigatures::Value(VariantLigatures::NONE)); + } + + while let Ok(flag) = input.try_parse(|input| { + Ok( + match_ignore_ascii_case! { &input.expect_ident().map_err(|_| ())?, + "common-ligatures" => + exclusive_value!((result, VariantLigatures::COMMON_LIGATURES | + VariantLigatures::NO_COMMON_LIGATURES + ) => VariantLigatures::COMMON_LIGATURES), + "no-common-ligatures" => + exclusive_value!((result, VariantLigatures::COMMON_LIGATURES | + VariantLigatures::NO_COMMON_LIGATURES + ) => VariantLigatures::NO_COMMON_LIGATURES), + "discretionary-ligatures" => + exclusive_value!((result, VariantLigatures::DISCRETIONARY_LIGATURES | + VariantLigatures::NO_DISCRETIONARY_LIGATURES + ) => VariantLigatures::DISCRETIONARY_LIGATURES), + "no-discretionary-ligatures" => + exclusive_value!((result, VariantLigatures::DISCRETIONARY_LIGATURES | + VariantLigatures::NO_DISCRETIONARY_LIGATURES + ) => VariantLigatures::NO_DISCRETIONARY_LIGATURES), + "historical-ligatures" => + exclusive_value!((result, VariantLigatures::HISTORICAL_LIGATURES | + VariantLigatures::NO_HISTORICAL_LIGATURES + ) => VariantLigatures::HISTORICAL_LIGATURES), + "no-historical-ligatures" => + exclusive_value!((result, VariantLigatures::HISTORICAL_LIGATURES | + VariantLigatures::NO_HISTORICAL_LIGATURES + ) => VariantLigatures::NO_HISTORICAL_LIGATURES), + "contextual" => + exclusive_value!((result, VariantLigatures::CONTEXTUAL | + VariantLigatures::NO_CONTEXTUAL + ) => VariantLigatures::CONTEXTUAL), + "no-contextual" => + exclusive_value!((result, VariantLigatures::CONTEXTUAL | + VariantLigatures::NO_CONTEXTUAL + ) => VariantLigatures::NO_CONTEXTUAL), + _ => return Err(()), + }, + ) + }) { + result.insert(flag); + } + + if !result.is_empty() { + Ok(FontVariantLigatures::Value(result)) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +macro_rules! impl_variant_numeric { + { + $( + $(#[$($meta:tt)+])* + $ident:ident / $css:expr => $gecko:ident = $value:expr, + )+ + } => { + bitflags! { + #[derive(MallocSizeOf, ToResolvedValue, ToShmem)] + /// Vairants of numeric values + pub struct VariantNumeric: u8 { + /// None of other variants are enabled. + const NORMAL = 0; + $( + $(#[$($meta)+])* + const $ident = $value; + )+ + } + } + + impl ToCss for VariantNumeric { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.is_empty() { + return dest.write_str("normal"); + } + + let mut writer = SequenceWriter::new(dest, " "); + $( + if self.intersects(VariantNumeric::$ident) { + writer.raw_item($css)?; + } + )+ + Ok(()) + } + } + + /// Asserts that all variant-east-asian matches its NS_FONT_VARIANT_EAST_ASIAN_* value. + #[cfg(feature = "gecko")] + #[inline] + pub fn assert_variant_numeric_matches() { + use crate::gecko_bindings::structs; + $( + debug_assert_eq!(structs::$gecko as u8, VariantNumeric::$ident.bits()); + )+ + } + + impl SpecifiedValueInfo for VariantNumeric { + fn collect_completion_keywords(f: KeywordsCollectFn) { + f(&["normal", $($css,)+]); + } + } + } +} + +impl_variant_numeric! { + /// Enables display of lining numerals. + LINING_NUMS / "lining-nums" => NS_FONT_VARIANT_NUMERIC_LINING = 0x01, + /// Enables display of old-style numerals. + OLDSTYLE_NUMS / "oldstyle-nums" => NS_FONT_VARIANT_NUMERIC_OLDSTYLE = 0x02, + /// Enables display of proportional numerals. + PROPORTIONAL_NUMS / "proportional-nums" => NS_FONT_VARIANT_NUMERIC_PROPORTIONAL = 0x04, + /// Enables display of tabular numerals. + TABULAR_NUMS / "tabular-nums" => NS_FONT_VARIANT_NUMERIC_TABULAR = 0x08, + /// Enables display of lining diagonal fractions. + DIAGONAL_FRACTIONS / "diagonal-fractions" => NS_FONT_VARIANT_NUMERIC_DIAGONAL_FRACTIONS = 0x10, + /// Enables display of lining stacked fractions. + STACKED_FRACTIONS / "stacked-fractions" => NS_FONT_VARIANT_NUMERIC_STACKED_FRACTIONS = 0x20, + /// Enables display of letter forms used with ordinal numbers. + ORDINAL / "ordinal" => NS_FONT_VARIANT_NUMERIC_ORDINAL = 0x80, + /// Enables display of slashed zeros. + SLASHED_ZERO / "slashed-zero" => NS_FONT_VARIANT_NUMERIC_SLASHZERO = 0x40, +} + +#[cfg(feature = "gecko")] +impl VariantNumeric { + /// Obtain a specified value from a Gecko keyword value + /// + /// Intended for use with presentation attributes, not style structs + pub fn from_gecko_keyword(kw: u8) -> Self { + Self::from_bits_truncate(kw) + } + + /// Transform into gecko keyword + pub fn to_gecko_keyword(self) -> u8 { + self.bits() + } +} + +#[cfg(feature = "gecko")] +impl_gecko_keyword_conversions!(VariantNumeric, u8); + +#[cfg_attr(feature = "gecko", derive(MallocSizeOf))] +#[derive(Clone, Copy, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +/// Specifies control over numerical forms. +pub enum FontVariantNumeric { + /// Value variant with `variant-numeric` + Value(VariantNumeric), + /// System font + #[css(skip)] + System(SystemFont), +} + +impl FontVariantNumeric { + #[inline] + /// Default value of `font-variant-numeric` as `empty` + pub fn empty() -> FontVariantNumeric { + FontVariantNumeric::Value(VariantNumeric::empty()) + } + + system_font_methods!(FontVariantNumeric, font_variant_numeric); +} + +impl ToComputedValue for FontVariantNumeric { + type ComputedValue = computed::FontVariantNumeric; + + fn to_computed_value(&self, context: &Context) -> computed::FontVariantNumeric { + match *self { + FontVariantNumeric::Value(ref v) => v.clone(), + FontVariantNumeric::System(_) => self.compute_system(context), + } + } + + fn from_computed_value(other: &computed::FontVariantNumeric) -> Self { + FontVariantNumeric::Value(other.clone()) + } +} + +impl Parse for FontVariantNumeric { + /// normal | + /// [ <numeric-figure-values> || + /// <numeric-spacing-values> || + /// <numeric-fraction-values> || + /// ordinal || + /// slashed-zero ] + /// <numeric-figure-values> = [ lining-nums | oldstyle-nums ] + /// <numeric-spacing-values> = [ proportional-nums | tabular-nums ] + /// <numeric-fraction-values> = [ diagonal-fractions | stacked-fractions ] + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontVariantNumeric, ParseError<'i>> { + let mut result = VariantNumeric::empty(); + + if input + .try_parse(|input| input.expect_ident_matching("normal")) + .is_ok() + { + return Ok(FontVariantNumeric::Value(result)); + } + + while let Ok(flag) = input.try_parse(|input| { + Ok( + match_ignore_ascii_case! { &input.expect_ident().map_err(|_| ())?, + "ordinal" => + exclusive_value!((result, VariantNumeric::ORDINAL) => VariantNumeric::ORDINAL), + "slashed-zero" => + exclusive_value!((result, VariantNumeric::SLASHED_ZERO) => VariantNumeric::SLASHED_ZERO), + "lining-nums" => + exclusive_value!((result, VariantNumeric::LINING_NUMS | + VariantNumeric::OLDSTYLE_NUMS + ) => VariantNumeric::LINING_NUMS), + "oldstyle-nums" => + exclusive_value!((result, VariantNumeric::LINING_NUMS | + VariantNumeric::OLDSTYLE_NUMS + ) => VariantNumeric::OLDSTYLE_NUMS), + "proportional-nums" => + exclusive_value!((result, VariantNumeric::PROPORTIONAL_NUMS | + VariantNumeric::TABULAR_NUMS + ) => VariantNumeric::PROPORTIONAL_NUMS), + "tabular-nums" => + exclusive_value!((result, VariantNumeric::PROPORTIONAL_NUMS | + VariantNumeric::TABULAR_NUMS + ) => VariantNumeric::TABULAR_NUMS), + "diagonal-fractions" => + exclusive_value!((result, VariantNumeric::DIAGONAL_FRACTIONS | + VariantNumeric::STACKED_FRACTIONS + ) => VariantNumeric::DIAGONAL_FRACTIONS), + "stacked-fractions" => + exclusive_value!((result, VariantNumeric::DIAGONAL_FRACTIONS | + VariantNumeric::STACKED_FRACTIONS + ) => VariantNumeric::STACKED_FRACTIONS), + _ => return Err(()), + }, + ) + }) { + result.insert(flag); + } + + if !result.is_empty() { + Ok(FontVariantNumeric::Value(result)) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +/// This property provides low-level control over OpenType or TrueType font features. +pub type SpecifiedFontFeatureSettings = FontSettings<FeatureTagValue<Integer>>; + +/// Define initial settings that apply when the font defined by an @font-face +/// rule is rendered. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum FontFeatureSettings { + /// Value of `FontSettings` + Value(SpecifiedFontFeatureSettings), + /// System font + #[css(skip)] + System(SystemFont), +} + +impl FontFeatureSettings { + #[inline] + /// Get default value of `font-feature-settings` as normal + pub fn normal() -> FontFeatureSettings { + FontFeatureSettings::Value(FontSettings::normal()) + } + + system_font_methods!(FontFeatureSettings, font_feature_settings); +} + +impl ToComputedValue for FontFeatureSettings { + type ComputedValue = computed::FontFeatureSettings; + + fn to_computed_value(&self, context: &Context) -> computed::FontFeatureSettings { + match *self { + FontFeatureSettings::Value(ref v) => v.to_computed_value(context), + FontFeatureSettings::System(_) => self.compute_system(context), + } + } + + fn from_computed_value(other: &computed::FontFeatureSettings) -> Self { + FontFeatureSettings::Value(ToComputedValue::from_computed_value(other)) + } +} + +impl Parse for FontFeatureSettings { + /// normal | <feature-tag-value># + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontFeatureSettings, ParseError<'i>> { + SpecifiedFontFeatureSettings::parse(context, input).map(FontFeatureSettings::Value) + } +} + +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +/// Whether user agents are allowed to synthesize bold or oblique font faces +/// when a font family lacks bold or italic faces +pub struct FontSynthesis { + /// If a `font-weight` is requested that the font family does not contain, + /// the user agent may synthesize the requested weight from the weights + /// that do exist in the font family. + #[css(represents_keyword)] + pub weight: bool, + /// If a font-style is requested that the font family does not contain, + /// the user agent may synthesize the requested style from the normal face in the font family. + #[css(represents_keyword)] + pub style: bool, +} + +impl FontSynthesis { + #[inline] + /// Get the default value of font-synthesis + pub fn get_initial_value() -> Self { + FontSynthesis { + weight: true, + style: true, + } + } +} + +impl Parse for FontSynthesis { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontSynthesis, ParseError<'i>> { + let mut result = FontSynthesis { + weight: false, + style: false, + }; + try_match_ident_ignore_ascii_case! { input, + "none" => Ok(result), + "weight" => { + result.weight = true; + if input.try_parse(|input| input.expect_ident_matching("style")).is_ok() { + result.style = true; + } + Ok(result) + }, + "style" => { + result.style = true; + if input.try_parse(|input| input.expect_ident_matching("weight")).is_ok() { + result.weight = true; + } + Ok(result) + }, + } + } +} + +impl ToCss for FontSynthesis { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.weight && self.style { + dest.write_str("weight style") + } else if self.style { + dest.write_str("style") + } else if self.weight { + dest.write_str("weight") + } else { + dest.write_str("none") + } + } +} + +#[cfg(feature = "gecko")] +impl From<u8> for FontSynthesis { + fn from(bits: u8) -> FontSynthesis { + use crate::gecko_bindings::structs; + + FontSynthesis { + weight: bits & structs::NS_FONT_SYNTHESIS_WEIGHT as u8 != 0, + style: bits & structs::NS_FONT_SYNTHESIS_STYLE as u8 != 0, + } + } +} + +#[cfg(feature = "gecko")] +impl From<FontSynthesis> for u8 { + fn from(v: FontSynthesis) -> u8 { + use crate::gecko_bindings::structs; + + let mut bits: u8 = 0; + if v.weight { + bits |= structs::NS_FONT_SYNTHESIS_WEIGHT as u8; + } + if v.style { + bits |= structs::NS_FONT_SYNTHESIS_STYLE as u8; + } + bits + } +} + +#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +/// Allows authors to explicitly specify the language system of the font, +/// overriding the language system implied by the content language +pub enum FontLanguageOverride { + /// When rendering with OpenType fonts, + /// the content language of the element is + /// used to infer the OpenType language system + Normal, + /// Single three-letter case-sensitive OpenType language system tag, + /// specifies the OpenType language system to be used instead of + /// the language system implied by the language of the element + Override(Box<str>), + /// Use system font + #[css(skip)] + System(SystemFont), +} + +impl FontLanguageOverride { + #[inline] + /// Get default value with `normal` + pub fn normal() -> FontLanguageOverride { + FontLanguageOverride::Normal + } + + /// The ToComputedValue implementation for non-system-font + /// FontLanguageOverride, used for @font-face descriptors. + #[inline] + pub fn compute_non_system(&self) -> computed::FontLanguageOverride { + match *self { + FontLanguageOverride::Normal => computed::FontLanguageOverride::zero(), + FontLanguageOverride::Override(ref lang) => { + computed::FontLanguageOverride::from_str(lang) + }, + FontLanguageOverride::System(..) => unreachable!(), + } + } + + system_font_methods!(FontLanguageOverride, font_language_override); +} + +impl ToComputedValue for FontLanguageOverride { + type ComputedValue = computed::FontLanguageOverride; + + #[inline] + fn to_computed_value(&self, context: &Context) -> computed::FontLanguageOverride { + match *self { + FontLanguageOverride::System(_) => self.compute_system(context), + _ => self.compute_non_system(), + } + } + #[inline] + fn from_computed_value(computed: &computed::FontLanguageOverride) -> Self { + if *computed == computed::FontLanguageOverride::zero() { + return FontLanguageOverride::Normal; + } + FontLanguageOverride::Override(computed.to_str(&mut [0; 4]).into()) + } +} + +impl Parse for FontLanguageOverride { + /// normal | <string> + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontLanguageOverride, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("normal")) + .is_ok() + { + return Ok(FontLanguageOverride::Normal); + } + + let string = input.expect_string()?; + Ok(FontLanguageOverride::Override( + string.as_ref().to_owned().into_boxed_str(), + )) + } +} + +/// This property provides low-level control over OpenType or TrueType font +/// variations. +pub type SpecifiedFontVariationSettings = FontSettings<VariationValue<Number>>; + +/// Define initial settings that apply when the font defined by an @font-face +/// rule is rendered. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum FontVariationSettings { + /// Value of `FontSettings` + Value(SpecifiedFontVariationSettings), + /// System font + #[css(skip)] + System(SystemFont), +} + +impl FontVariationSettings { + #[inline] + /// Get default value of `font-variation-settings` as normal + pub fn normal() -> FontVariationSettings { + FontVariationSettings::Value(FontSettings::normal()) + } + + system_font_methods!(FontVariationSettings, font_variation_settings); +} + +impl ToComputedValue for FontVariationSettings { + type ComputedValue = computed::FontVariationSettings; + + fn to_computed_value(&self, context: &Context) -> computed::FontVariationSettings { + match *self { + FontVariationSettings::Value(ref v) => v.to_computed_value(context), + FontVariationSettings::System(_) => self.compute_system(context), + } + } + + fn from_computed_value(other: &computed::FontVariationSettings) -> Self { + FontVariationSettings::Value(ToComputedValue::from_computed_value(other)) + } +} + +impl Parse for FontVariationSettings { + /// normal | <variation-tag-value># + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontVariationSettings, ParseError<'i>> { + SpecifiedFontVariationSettings::parse(context, input).map(FontVariationSettings::Value) + } +} + +fn parse_one_feature_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, +) -> Result<Integer, ParseError<'i>> { + if let Ok(integer) = input.try_parse(|i| Integer::parse_non_negative(context, i)) { + return Ok(integer); + } + + try_match_ident_ignore_ascii_case! { input, + "on" => Ok(Integer::new(1)), + "off" => Ok(Integer::new(0)), + } +} + +impl Parse for FeatureTagValue<Integer> { + /// https://drafts.csswg.org/css-fonts-4/#feature-tag-value + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let tag = FontTag::parse(context, input)?; + let value = input + .try_parse(|i| parse_one_feature_value(context, i)) + .unwrap_or_else(|_| Integer::new(1)); + + Ok(Self { tag, value }) + } +} + +impl Parse for VariationValue<Number> { + /// This is the `<string> <number>` part of the font-variation-settings + /// syntax. + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let tag = FontTag::parse(context, input)?; + let value = Number::parse(context, input)?; + Ok(Self { tag, value }) + } +} + +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +/// text-zoom. Enable if true, disable if false +pub struct XTextZoom(#[css(skip)] pub bool); + +impl Parse for XTextZoom { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<XTextZoom, ParseError<'i>> { + debug_assert!( + false, + "Should be set directly by presentation attributes only." + ); + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } +} + +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +/// Internal property that reflects the lang attribute +pub struct XLang(#[css(skip)] pub Atom); + +impl XLang { + #[inline] + /// Get default value for `-x-lang` + pub fn get_initial_value() -> XLang { + XLang(atom!("")) + } +} + +impl Parse for XLang { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<XLang, ParseError<'i>> { + debug_assert!( + false, + "Should be set directly by presentation attributes only." + ); + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } +} + +#[cfg_attr(feature = "gecko", derive(MallocSizeOf))] +#[derive(Clone, Copy, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +/// Specifies the minimum font size allowed due to changes in scriptlevel. +/// Ref: https://wiki.mozilla.org/MathML:mstyle +pub struct MozScriptMinSize(pub NoCalcLength); + +impl MozScriptMinSize { + #[inline] + /// Calculate initial value of -moz-script-min-size. + pub fn get_initial_value() -> Length { + Length::new(DEFAULT_SCRIPT_MIN_SIZE_PT as f32 * (AU_PER_PT / AU_PER_PX)) + } +} + +impl Parse for MozScriptMinSize { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<MozScriptMinSize, ParseError<'i>> { + debug_assert!( + false, + "Should be set directly by presentation attributes only." + ); + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } +} + +/// A value for the `math-depth` property. +/// https://mathml-refresh.github.io/mathml-core/#the-math-script-level-property +#[cfg_attr(feature = "gecko", derive(MallocSizeOf))] +#[derive(Clone, Copy, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum MathDepth { + /// Increment math-depth if math-style is compact. + AutoAdd, + + /// Add the function's argument to math-depth. + #[css(function)] + Add(Integer), + + /// Set math-depth to the specified value. + Absolute(Integer), +} + +impl Parse for MathDepth { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<MathDepth, ParseError<'i>> { + if input + .try_parse(|i| i.expect_ident_matching("auto-add")) + .is_ok() + { + return Ok(MathDepth::AutoAdd); + } + if let Ok(math_depth_value) = input.try_parse(|input| Integer::parse(context, input)) { + return Ok(MathDepth::Absolute(math_depth_value)); + } + input.expect_function_matching("add")?; + let math_depth_delta_value = + input.parse_nested_block(|input| Integer::parse(context, input))?; + Ok(MathDepth::Add(math_depth_delta_value)) + } +} + +#[cfg_attr(feature = "gecko", derive(MallocSizeOf))] +#[derive( + Clone, + Copy, + Debug, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +/// Specifies the multiplier to be used to adjust font size +/// due to changes in scriptlevel. +/// +/// Ref: https://www.w3.org/TR/MathML3/chapter3.html#presm.mstyle.attrs +pub struct MozScriptSizeMultiplier(pub f32); + +impl MozScriptSizeMultiplier { + #[inline] + /// Get default value of `-moz-script-size-multiplier` + pub fn get_initial_value() -> MozScriptSizeMultiplier { + MozScriptSizeMultiplier(DEFAULT_SCRIPT_SIZE_MULTIPLIER as f32) + } +} + +impl Parse for MozScriptSizeMultiplier { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<MozScriptSizeMultiplier, ParseError<'i>> { + debug_assert!( + false, + "Should be set directly by presentation attributes only." + ); + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } +} + +impl From<f32> for MozScriptSizeMultiplier { + fn from(v: f32) -> Self { + MozScriptSizeMultiplier(v) + } +} + +impl From<MozScriptSizeMultiplier> for f32 { + fn from(v: MozScriptSizeMultiplier) -> f32 { + v.0 + } +} diff --git a/servo/components/style/values/specified/gecko.rs b/servo/components/style/values/specified/gecko.rs new file mode 100644 index 0000000000..3e3085c884 --- /dev/null +++ b/servo/components/style/values/specified/gecko.rs @@ -0,0 +1,75 @@ +/* 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/. */ + +//! Specified types for legacy Gecko-only properties. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::{self, Length, LengthPercentage}; +use crate::values::generics::rect::Rect; +use cssparser::{Parser, Token}; +use std::fmt; +use style_traits::values::SequenceWriter; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +fn parse_pixel_or_percent<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, +) -> Result<LengthPercentage, ParseError<'i>> { + let location = input.current_source_location(); + let token = input.next()?; + let value = match *token { + Token::Dimension { + value, ref unit, .. + } => { + match_ignore_ascii_case! { unit, + "px" => Ok(LengthPercentage::new_length(Length::new(value))), + _ => Err(()), + } + }, + Token::Percentage { unit_value, .. } => Ok(LengthPercentage::new_percent( + computed::Percentage(unit_value), + )), + _ => Err(()), + }; + value.map_err(|()| location.new_custom_error(StyleParseErrorKind::UnspecifiedError)) +} + +/// The value of an IntersectionObserver's rootMargin property. +/// +/// Only bare px or percentage values are allowed. Other length units and +/// calc() values are not allowed. +/// +/// <https://w3c.github.io/IntersectionObserver/#parse-a-root-margin> +#[repr(transparent)] +pub struct IntersectionObserverRootMargin(pub Rect<LengthPercentage>); + +impl Parse for IntersectionObserverRootMargin { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let rect = Rect::parse_with(context, input, parse_pixel_or_percent)?; + Ok(IntersectionObserverRootMargin(rect)) + } +} + +// Strictly speaking this is not ToCss. It's serializing for DOM. But +// we can just reuse the infrastructure of this. +// +// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-rootmargin> +impl ToCss for IntersectionObserverRootMargin { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + // We cannot use the ToCss impl of Rect, because that would + // merge items when they are equal. We want to list them all. + let mut writer = SequenceWriter::new(dest, " "); + let rect = &self.0; + writer.item(&rect.0)?; + writer.item(&rect.1)?; + writer.item(&rect.2)?; + writer.item(&rect.3) + } +} diff --git a/servo/components/style/values/specified/grid.rs b/servo/components/style/values/specified/grid.rs new file mode 100644 index 0000000000..cfcba5dca3 --- /dev/null +++ b/servo/components/style/values/specified/grid.rs @@ -0,0 +1,349 @@ +/* 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/. */ + +//! CSS handling for the computed value of +//! [grids](https://drafts.csswg.org/css-grid/) + +use crate::parser::{Parse, ParserContext}; +use crate::values::generics::grid::{GridTemplateComponent, ImplicitGridTracks, RepeatCount}; +use crate::values::generics::grid::{LineNameList, TrackBreadth, TrackRepeat, TrackSize}; +use crate::values::generics::grid::{TrackList, TrackListValue}; +use crate::values::specified::{Integer, LengthPercentage}; +use crate::values::{CSSFloat, CustomIdent}; +use cssparser::{ParseError as CssParseError, Parser, Token}; +use std::mem; +use style_traits::{ParseError, StyleParseErrorKind}; + +/// Parse a single flexible length. +pub fn parse_flex<'i, 't>(input: &mut Parser<'i, 't>) -> Result<CSSFloat, ParseError<'i>> { + let location = input.current_source_location(); + match *input.next()? { + Token::Dimension { + value, ref unit, .. + } if unit.eq_ignore_ascii_case("fr") && value.is_sign_positive() => Ok(value), + ref t => Err(location.new_unexpected_token_error(t.clone())), + } +} + +impl<L> TrackBreadth<L> { + fn parse_keyword<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> { + #[derive(Parse)] + enum TrackKeyword { + Auto, + MaxContent, + MinContent, + } + + Ok(match TrackKeyword::parse(input)? { + TrackKeyword::Auto => TrackBreadth::Auto, + TrackKeyword::MaxContent => TrackBreadth::MaxContent, + TrackKeyword::MinContent => TrackBreadth::MinContent, + }) + } +} + +impl Parse for TrackBreadth<LengthPercentage> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // FIXME: This and other callers in this file should use + // NonNegativeLengthPercentage instead. + // + // Though it seems these cannot be animated so it's ~ok. + if let Ok(lp) = input.try_parse(|i| LengthPercentage::parse_non_negative(context, i)) { + return Ok(TrackBreadth::Breadth(lp)); + } + + if let Ok(f) = input.try_parse(parse_flex) { + return Ok(TrackBreadth::Fr(f)); + } + + Self::parse_keyword(input) + } +} + +impl Parse for TrackSize<LengthPercentage> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(b) = input.try_parse(|i| TrackBreadth::parse(context, i)) { + return Ok(TrackSize::Breadth(b)); + } + + if input + .try_parse(|i| i.expect_function_matching("minmax")) + .is_ok() + { + return input.parse_nested_block(|input| { + let inflexible_breadth = + match input.try_parse(|i| LengthPercentage::parse_non_negative(context, i)) { + Ok(lp) => TrackBreadth::Breadth(lp), + Err(..) => TrackBreadth::parse_keyword(input)?, + }; + + input.expect_comma()?; + Ok(TrackSize::Minmax( + inflexible_breadth, + TrackBreadth::parse(context, input)?, + )) + }); + } + + input.expect_function_matching("fit-content")?; + let lp = input.parse_nested_block(|i| LengthPercentage::parse_non_negative(context, i))?; + Ok(TrackSize::FitContent(TrackBreadth::Breadth(lp))) + } +} + +impl Parse for ImplicitGridTracks<TrackSize<LengthPercentage>> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + use style_traits::{Separator, Space}; + let track_sizes = Space::parse(input, |i| TrackSize::parse(context, i))?; + if track_sizes.len() == 1 && track_sizes[0].is_initial() { + // A single track with the initial value is always represented by an empty slice. + return Ok(Default::default()); + } + return Ok(ImplicitGridTracks(track_sizes.into())); + } +} + +/// Parse the grid line names into a vector of owned strings. +/// +/// <https://drafts.csswg.org/css-grid/#typedef-line-names> +pub fn parse_line_names<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<crate::OwnedSlice<CustomIdent>, ParseError<'i>> { + input.expect_square_bracket_block()?; + input.parse_nested_block(|input| { + let mut values = vec![]; + while let Ok((loc, ident)) = input.try_parse(|i| -> Result<_, CssParseError<()>> { + Ok((i.current_source_location(), i.expect_ident_cloned()?)) + }) { + let ident = CustomIdent::from_ident(loc, &ident, &["span", "auto"])?; + values.push(ident); + } + + Ok(values.into()) + }) +} + +/// The type of `repeat` function (only used in parsing). +/// +/// <https://drafts.csswg.org/css-grid/#typedef-track-repeat> +#[derive(Clone, Copy, Debug, PartialEq, SpecifiedValueInfo)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +enum RepeatType { + /// [`<auto-repeat>`](https://drafts.csswg.org/css-grid/#typedef-auto-repeat) + Auto, + /// [`<track-repeat>`](https://drafts.csswg.org/css-grid/#typedef-track-repeat) + Normal, + /// [`<fixed-repeat>`](https://drafts.csswg.org/css-grid/#typedef-fixed-repeat) + Fixed, +} + +impl TrackRepeat<LengthPercentage, Integer> { + fn parse_with_repeat_type<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<(Self, RepeatType), ParseError<'i>> { + input + .try_parse(|i| i.expect_function_matching("repeat").map_err(|e| e.into())) + .and_then(|_| { + input.parse_nested_block(|input| { + let count = RepeatCount::parse(context, input)?; + input.expect_comma()?; + + let is_auto = count == RepeatCount::AutoFit || count == RepeatCount::AutoFill; + let mut repeat_type = if is_auto { + RepeatType::Auto + } else { + // <fixed-size> is a subset of <track-size>, so it should work for both + RepeatType::Fixed + }; + + let mut names = vec![]; + let mut values = vec![]; + let mut current_names; + + loop { + current_names = input.try_parse(parse_line_names).unwrap_or_default(); + if let Ok(track_size) = input.try_parse(|i| TrackSize::parse(context, i)) { + if !track_size.is_fixed() { + if is_auto { + // should be <fixed-size> for <auto-repeat> + return Err(input + .new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + if repeat_type == RepeatType::Fixed { + repeat_type = RepeatType::Normal // <track-size> for sure + } + } + + values.push(track_size); + names.push(current_names); + } else { + if values.is_empty() { + // expecting at least one <track-size> + return Err( + input.new_custom_error(StyleParseErrorKind::UnspecifiedError) + ); + } + + names.push(current_names); // final `<line-names>` + break; // no more <track-size>, breaking + } + } + + let repeat = TrackRepeat { + count, + track_sizes: values.into(), + line_names: names.into(), + }; + + Ok((repeat, repeat_type)) + }) + }) + } +} + +impl Parse for TrackList<LengthPercentage, Integer> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut current_names = vec![]; + let mut names = vec![]; + let mut values = vec![]; + + // Whether we've parsed an `<auto-repeat>` value. + let mut auto_repeat_index = None; + // assume that everything is <fixed-size>. This flag is useful when we encounter <auto-repeat> + let mut at_least_one_not_fixed = false; + loop { + current_names + .extend_from_slice(&mut input.try_parse(parse_line_names).unwrap_or_default()); + if let Ok(track_size) = input.try_parse(|i| TrackSize::parse(context, i)) { + if !track_size.is_fixed() { + at_least_one_not_fixed = true; + if auto_repeat_index.is_some() { + // <auto-track-list> only accepts <fixed-size> and <fixed-repeat> + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + } + + let vec = mem::replace(&mut current_names, vec![]); + names.push(vec.into()); + values.push(TrackListValue::TrackSize(track_size)); + } else if let Ok((repeat, type_)) = + input.try_parse(|i| TrackRepeat::parse_with_repeat_type(context, i)) + { + match type_ { + RepeatType::Normal => { + at_least_one_not_fixed = true; + if auto_repeat_index.is_some() { + // only <fixed-repeat> + return Err( + input.new_custom_error(StyleParseErrorKind::UnspecifiedError) + ); + } + }, + RepeatType::Auto => { + if auto_repeat_index.is_some() || at_least_one_not_fixed { + // We've either seen <auto-repeat> earlier, or there's at least one non-fixed value + return Err( + input.new_custom_error(StyleParseErrorKind::UnspecifiedError) + ); + } + auto_repeat_index = Some(values.len()); + }, + RepeatType::Fixed => {}, + } + + let vec = mem::replace(&mut current_names, vec![]); + names.push(vec.into()); + values.push(TrackListValue::TrackRepeat(repeat)); + } else { + if values.is_empty() && auto_repeat_index.is_none() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + names.push(current_names.into()); + break; + } + } + + Ok(TrackList { + auto_repeat_index: auto_repeat_index.unwrap_or(std::usize::MAX), + values: values.into(), + line_names: names.into(), + }) + } +} + +#[cfg(feature = "gecko")] +#[inline] +fn allow_grid_template_subgrids() -> bool { + static_prefs::pref!("layout.css.grid-template-subgrid-value.enabled") +} + +#[cfg(feature = "servo")] +#[inline] +fn allow_grid_template_subgrids() -> bool { + false +} + +#[cfg(feature = "gecko")] +#[inline] +fn allow_grid_template_masonry() -> bool { + static_prefs::pref!("layout.css.grid-template-masonry-value.enabled") +} + +#[cfg(feature = "servo")] +#[inline] +fn allow_grid_template_masonry() -> bool { + false +} + +impl Parse for GridTemplateComponent<LengthPercentage, Integer> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(GridTemplateComponent::None); + } + + Self::parse_without_none(context, input) + } +} + +impl GridTemplateComponent<LengthPercentage, Integer> { + /// Parses a `GridTemplateComponent<LengthPercentage>` except `none` keyword. + pub fn parse_without_none<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if allow_grid_template_subgrids() { + if let Ok(t) = input.try_parse(|i| LineNameList::parse(context, i)) { + return Ok(GridTemplateComponent::Subgrid(Box::new(t))); + } + } + if allow_grid_template_masonry() { + if input + .try_parse(|i| i.expect_ident_matching("masonry")) + .is_ok() + { + return Ok(GridTemplateComponent::Masonry); + } + } + let track_list = TrackList::parse(context, input)?; + Ok(GridTemplateComponent::TrackList(Box::new(track_list))) + } +} diff --git a/servo/components/style/values/specified/image.rs b/servo/components/style/values/specified/image.rs new file mode 100644 index 0000000000..2fa8d0bb1e --- /dev/null +++ b/servo/components/style/values/specified/image.rs @@ -0,0 +1,1170 @@ +/* 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/. */ + +//! CSS handling for the specified value of +//! [`image`][image]s +//! +//! [image]: https://drafts.csswg.org/css-images/#image-values + +use crate::custom_properties::SpecifiedValue; +use crate::parser::{Parse, ParserContext}; +use crate::stylesheets::CorsMode; +use crate::values::generics::image::PaintWorklet; +use crate::values::generics::image::{ + self as generic, Circle, Ellipse, GradientCompatMode, ShapeExtent, +}; +use crate::values::generics::position::Position as GenericPosition; +use crate::values::generics::NonNegative; +use crate::values::specified::position::{HorizontalPositionKeyword, VerticalPositionKeyword}; +use crate::values::specified::position::{Position, PositionComponent, Side}; +use crate::values::specified::url::SpecifiedImageUrl; +use crate::values::specified::{ + Angle, AngleOrPercentage, Color, Length, LengthPercentage, NonNegativeLength, + NonNegativeLengthPercentage, Resolution +}; +use crate::values::specified::{Number, NumberOrPercentage, Percentage}; +use crate::Atom; +use cssparser::{Delimiter, Parser, Token}; +use selectors::parser::SelectorParseErrorKind; +#[cfg(feature = "servo")] +use servo_url::ServoUrl; +use std::cmp::Ordering; +use std::fmt::{self, Write}; +use style_traits::{CssType, CssWriter, KeywordsCollectFn, ParseError}; +use style_traits::{SpecifiedValueInfo, StyleParseErrorKind, ToCss}; + +/// Specified values for an image according to CSS-IMAGES. +/// <https://drafts.csswg.org/css-images/#image-values> +pub type Image = generic::Image<Gradient, MozImageRect, SpecifiedImageUrl, Color, Percentage, Resolution>; + +/// Specified values for a CSS gradient. +/// <https://drafts.csswg.org/css-images/#gradients> +pub type Gradient = generic::Gradient< + LineDirection, + LengthPercentage, + NonNegativeLength, + NonNegativeLengthPercentage, + Position, + Angle, + AngleOrPercentage, + Color, +>; + +/// Specified values for CSS cross-fade +/// cross-fade( CrossFadeElement, ...) +/// <https://drafts.csswg.org/css-images-4/#cross-fade-function> +pub type CrossFade = generic::CrossFade<Image, Color, Percentage>; +/// CrossFadeElement = percent? CrossFadeImage +pub type CrossFadeElement = generic::CrossFadeElement<Image, Color, Percentage>; +/// CrossFadeImage = image | color +pub type CrossFadeImage = generic::CrossFadeImage<Image, Color>; +/// A specified percentage or nothing. +pub type PercentOrNone = generic::PercentOrNone<Percentage>; + +/// `image-set()` +pub type ImageSet = generic::ImageSet<Image, Resolution>; + +/// Each of the arguments to `image-set()` +pub type ImageSetItem = generic::ImageSetItem<Image, Resolution>; + +type LengthPercentageItemList = crate::OwnedSlice<generic::GradientItem<Color, LengthPercentage>>; + +#[cfg(feature = "gecko")] +fn cross_fade_enabled() -> bool { + static_prefs::pref!("layout.css.cross-fade.enabled") +} + +#[cfg(feature = "servo")] +fn cross_fade_enabled() -> bool { + false +} + +#[cfg(feature = "gecko")] +fn image_set_enabled() -> bool { + static_prefs::pref!("layout.css.image-set.enabled") +} + +#[cfg(feature = "servo")] +fn image_set_enabled() -> bool { + false +} + +impl SpecifiedValueInfo for Gradient { + const SUPPORTED_TYPES: u8 = CssType::GRADIENT; + + fn collect_completion_keywords(f: KeywordsCollectFn) { + // This list here should keep sync with that in Gradient::parse. + f(&[ + "linear-gradient", + "-webkit-linear-gradient", + "-moz-linear-gradient", + "repeating-linear-gradient", + "-webkit-repeating-linear-gradient", + "-moz-repeating-linear-gradient", + "radial-gradient", + "-webkit-radial-gradient", + "-moz-radial-gradient", + "repeating-radial-gradient", + "-webkit-repeating-radial-gradient", + "-moz-repeating-radial-gradient", + "-webkit-gradient", + "conic-gradient", + "repeating-conic-gradient", + ]); + } +} + +// Need to manually implement as whether or not cross-fade shows up in +// completions & etc is dependent on it being enabled. +impl<Image, Color, Percentage> SpecifiedValueInfo for generic::CrossFade<Image, Color, Percentage> { + const SUPPORTED_TYPES: u8 = 0; + + fn collect_completion_keywords(f: KeywordsCollectFn) { + if cross_fade_enabled() { + f(&["cross-fade"]); + } + } +} + +impl<Image, Resolution> SpecifiedValueInfo for generic::ImageSet<Image, Resolution> { + const SUPPORTED_TYPES: u8 = 0; + + fn collect_completion_keywords(f: KeywordsCollectFn) { + if image_set_enabled() { + f(&["image-set"]); + } + } +} + +/// A specified gradient line direction. +/// +/// FIXME(emilio): This should be generic over Angle. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub enum LineDirection { + /// An angular direction. + Angle(Angle), + /// A horizontal direction. + Horizontal(HorizontalPositionKeyword), + /// A vertical direction. + Vertical(VerticalPositionKeyword), + /// A direction towards a corner of a box. + Corner(HorizontalPositionKeyword, VerticalPositionKeyword), +} + +/// A specified ending shape. +pub type EndingShape = generic::EndingShape<NonNegativeLength, NonNegativeLengthPercentage>; + +/// Specified values for `moz-image-rect` +/// -moz-image-rect(<uri>, top, right, bottom, left); +#[cfg(all(feature = "gecko", not(feature = "cbindgen")))] +pub type MozImageRect = generic::GenericMozImageRect<NumberOrPercentage, SpecifiedImageUrl>; + +#[cfg(not(feature = "gecko"))] +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +/// Empty enum on non-Gecko +pub enum MozImageRect {} + +impl Parse for Image { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Image, ParseError<'i>> { + Image::parse_with_cors_mode(context, input, CorsMode::None, /* allow_none = */ true) + } +} + +impl Image { + fn parse_with_cors_mode<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + allow_none: bool, + ) -> Result<Image, ParseError<'i>> { + if allow_none && input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(generic::Image::None); + } + if let Ok(url) = input.try_parse(|input| SpecifiedImageUrl::parse_with_cors_mode(context, input, cors_mode)) { + return Ok(generic::Image::Url(url)); + } + if let Ok(gradient) = input.try_parse(|i| Gradient::parse(context, i)) { + return Ok(generic::Image::Gradient(Box::new(gradient))); + } + if image_set_enabled() { + if let Ok(is) = input.try_parse(|input| ImageSet::parse(context, input, cors_mode)) { + return Ok(generic::Image::ImageSet(Box::new(is))); + } + } + if cross_fade_enabled() { + if let Ok(cf) = input.try_parse(|input| CrossFade::parse(context, input, cors_mode)) { + return Ok(generic::Image::CrossFade(Box::new(cf))); + } + } + #[cfg(feature = "servo-layout-2013")] + { + if let Ok(paint_worklet) = input.try_parse(|i| PaintWorklet::parse(context, i)) { + return Ok(generic::Image::PaintWorklet(paint_worklet)); + } + } + #[cfg(feature = "gecko")] + { + if let Ok(image_rect) = input.try_parse(|input| MozImageRect::parse(context, input, cors_mode)) { + return Ok(generic::Image::Rect(Box::new(image_rect))); + } + Ok(generic::Image::Element(Image::parse_element(input)?)) + } + #[cfg(not(feature = "gecko"))] + Err(input.new_error_for_next_token()) + } +} + +impl Image { + /// Creates an already specified image value from an already resolved URL + /// for insertion in the cascade. + #[cfg(feature = "servo")] + pub fn for_cascade(url: ServoUrl) -> Self { + use crate::values::CssUrl; + generic::Image::Url(CssUrl::for_cascade(url)) + } + + /// Parses a `-moz-element(# <element-id>)`. + #[cfg(feature = "gecko")] + fn parse_element<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Atom, ParseError<'i>> { + input.try_parse(|i| i.expect_function_matching("-moz-element"))?; + let location = input.current_source_location(); + input.parse_nested_block(|i| match *i.next()? { + Token::IDHash(ref id) => Ok(Atom::from(id.as_ref())), + ref t => Err(location.new_unexpected_token_error(t.clone())), + }) + } + + /// Provides an alternate method for parsing that associates the URL with + /// anonymous CORS headers. + pub fn parse_with_cors_anonymous<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Image, ParseError<'i>> { + Self::parse_with_cors_mode(context, input, CorsMode::Anonymous, /* allow_none = */ true) + } +} + +impl CrossFade { + /// cross-fade() = cross-fade( <cf-image># ) + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + ) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("cross-fade")?; + let elements = input.parse_nested_block(|input| { + input.parse_comma_separated(|input| CrossFadeElement::parse(context, input, cors_mode)) + })?; + let elements = crate::OwnedSlice::from(elements); + Ok(Self { elements }) + } +} + +impl CrossFadeElement { + /// <cf-image> = <percentage>? && [ <image> | <color> ] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + ) -> Result<Self, ParseError<'i>> { + // Try and parse a leading percent sign. + let mut percent = PercentOrNone::parse_or_none(context, input); + // Parse the image + let image = CrossFadeImage::parse(context, input, cors_mode)?; + // Try and parse a trailing percent sign. + if percent == PercentOrNone::None { + percent = PercentOrNone::parse_or_none(context, input); + } + Ok(Self { percent, image }) + } +} + +impl CrossFadeImage { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + ) -> Result<Self, ParseError<'i>> { + if let Ok(image) = input.try_parse(|input| Image::parse_with_cors_mode(context, input, cors_mode, /* allow_none = */ false)) { + return Ok(Self::Image(image)) + } + Ok(Self::Color(Color::parse(context, input)?)) + } +} + +impl PercentOrNone { + fn parse_or_none<'i, 't>(context: &ParserContext, input: &mut Parser<'i, 't>) -> Self { + // We clamp our values here as this is the way that Safari and + // Chrome's implementation handle out-of-bounds percentages + // but whether or not this behavior follows the specification + // is still being discussed. See: + // <https://github.com/w3c/csswg-drafts/issues/5333> + if let Ok(percent) = input.try_parse(|input| Percentage::parse_non_negative(context, input)) + { + Self::Percent(percent.clamp_to_hundred()) + } else { + Self::None + } + } +} + +impl ImageSet { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + ) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("image-set")?; + let items = input.parse_nested_block(|input| { + input.parse_comma_separated(|input| ImageSetItem::parse(context, input, cors_mode)) + })?; + Ok(Self { + selected_index: 0, + items: items.into() + }) + } +} + +impl ImageSetItem { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + ) -> Result<Self, ParseError<'i>> { + let image = match input.try_parse(|i| i.expect_url_or_string()) { + Ok(url) => Image::Url(SpecifiedImageUrl::parse_from_string(url.as_ref().into(), context, cors_mode)), + Err(..) => Image::parse_with_cors_mode(context, input, cors_mode, /* allow_none = */ false)?, + }; + let resolution = input.try_parse(|input| Resolution::parse(context, input)).unwrap_or(Resolution::X(1.0)); + Ok(Self { + image, + resolution, + }) + } +} + +impl Parse for Gradient { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + enum Shape { + Linear, + Radial, + Conic, + } + + let func = input.expect_function()?; + let (shape, repeating, compat_mode) = match_ignore_ascii_case! { &func, + "linear-gradient" => { + (Shape::Linear, false, GradientCompatMode::Modern) + }, + "-webkit-linear-gradient" => { + (Shape::Linear, false, GradientCompatMode::WebKit) + }, + #[cfg(feature = "gecko")] + "-moz-linear-gradient" => { + (Shape::Linear, false, GradientCompatMode::Moz) + }, + "repeating-linear-gradient" => { + (Shape::Linear, true, GradientCompatMode::Modern) + }, + "-webkit-repeating-linear-gradient" => { + (Shape::Linear, true, GradientCompatMode::WebKit) + }, + #[cfg(feature = "gecko")] + "-moz-repeating-linear-gradient" => { + (Shape::Linear, true, GradientCompatMode::Moz) + }, + "radial-gradient" => { + (Shape::Radial, false, GradientCompatMode::Modern) + }, + "-webkit-radial-gradient" => { + (Shape::Radial, false, GradientCompatMode::WebKit) + }, + #[cfg(feature = "gecko")] + "-moz-radial-gradient" => { + (Shape::Radial, false, GradientCompatMode::Moz) + }, + "repeating-radial-gradient" => { + (Shape::Radial, true, GradientCompatMode::Modern) + }, + "-webkit-repeating-radial-gradient" => { + (Shape::Radial, true, GradientCompatMode::WebKit) + }, + #[cfg(feature = "gecko")] + "-moz-repeating-radial-gradient" => { + (Shape::Radial, true, GradientCompatMode::Moz) + }, + "conic-gradient" => { + (Shape::Conic, false, GradientCompatMode::Modern) + }, + "repeating-conic-gradient" => { + (Shape::Conic, true, GradientCompatMode::Modern) + }, + "-webkit-gradient" => { + return input.parse_nested_block(|i| { + Self::parse_webkit_gradient_argument(context, i) + }); + }, + _ => { + let func = func.clone(); + return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedFunction(func))); + } + }; + + Ok(input.parse_nested_block(|i| { + Ok(match shape { + Shape::Linear => Self::parse_linear(context, i, repeating, compat_mode)?, + Shape::Radial => Self::parse_radial(context, i, repeating, compat_mode)?, + Shape::Conic => Self::parse_conic(context, i, repeating)?, + }) + })?) + } +} + +impl Gradient { + fn parse_webkit_gradient_argument<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + use crate::values::specified::position::{ + HorizontalPositionKeyword as X, VerticalPositionKeyword as Y, + }; + type Point = GenericPosition<Component<X>, Component<Y>>; + + #[derive(Clone, Copy, Parse)] + enum Component<S> { + Center, + Number(NumberOrPercentage), + Side(S), + } + + impl LineDirection { + fn from_points(first: Point, second: Point) -> Self { + let h_ord = first.horizontal.partial_cmp(&second.horizontal); + let v_ord = first.vertical.partial_cmp(&second.vertical); + let (h, v) = match (h_ord, v_ord) { + (Some(h), Some(v)) => (h, v), + _ => return LineDirection::Vertical(Y::Bottom), + }; + match (h, v) { + (Ordering::Less, Ordering::Less) => LineDirection::Corner(X::Right, Y::Bottom), + (Ordering::Less, Ordering::Equal) => LineDirection::Horizontal(X::Right), + (Ordering::Less, Ordering::Greater) => LineDirection::Corner(X::Right, Y::Top), + (Ordering::Equal, Ordering::Greater) => LineDirection::Vertical(Y::Top), + (Ordering::Equal, Ordering::Equal) | (Ordering::Equal, Ordering::Less) => { + LineDirection::Vertical(Y::Bottom) + }, + (Ordering::Greater, Ordering::Less) => { + LineDirection::Corner(X::Left, Y::Bottom) + }, + (Ordering::Greater, Ordering::Equal) => LineDirection::Horizontal(X::Left), + (Ordering::Greater, Ordering::Greater) => { + LineDirection::Corner(X::Left, Y::Top) + }, + } + } + } + + impl From<Point> for Position { + fn from(point: Point) -> Self { + Self::new(point.horizontal.into(), point.vertical.into()) + } + } + + impl Parse for Point { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + input.try_parse(|i| { + let x = Component::parse(context, i)?; + let y = Component::parse(context, i)?; + + Ok(Self::new(x, y)) + }) + } + } + + impl<S: Side> From<Component<S>> for NumberOrPercentage { + fn from(component: Component<S>) -> Self { + match component { + Component::Center => NumberOrPercentage::Percentage(Percentage::new(0.5)), + Component::Number(number) => number, + Component::Side(side) => { + let p = if side.is_start() { + Percentage::zero() + } else { + Percentage::hundred() + }; + NumberOrPercentage::Percentage(p) + }, + } + } + } + + impl<S: Side> From<Component<S>> for PositionComponent<S> { + fn from(component: Component<S>) -> Self { + match component { + Component::Center => PositionComponent::Center, + Component::Number(NumberOrPercentage::Number(number)) => { + PositionComponent::Length(Length::from_px(number.value).into()) + }, + Component::Number(NumberOrPercentage::Percentage(p)) => { + PositionComponent::Length(p.into()) + }, + Component::Side(side) => PositionComponent::Side(side, None), + } + } + } + + impl<S: Copy + Side> Component<S> { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + match ( + NumberOrPercentage::from(*self), + NumberOrPercentage::from(*other), + ) { + (NumberOrPercentage::Percentage(a), NumberOrPercentage::Percentage(b)) => { + a.get().partial_cmp(&b.get()) + }, + (NumberOrPercentage::Number(a), NumberOrPercentage::Number(b)) => { + a.value.partial_cmp(&b.value) + }, + (_, _) => None, + } + } + } + + let ident = input.expect_ident_cloned()?; + input.expect_comma()?; + + Ok(match_ignore_ascii_case! { &ident, + "linear" => { + let first = Point::parse(context, input)?; + input.expect_comma()?; + let second = Point::parse(context, input)?; + + let direction = LineDirection::from_points(first, second); + let items = Gradient::parse_webkit_gradient_stops(context, input, false)?; + + generic::Gradient::Linear { + direction, + items, + repeating: false, + compat_mode: GradientCompatMode::Modern, + } + }, + "radial" => { + let first_point = Point::parse(context, input)?; + input.expect_comma()?; + let first_radius = Number::parse_non_negative(context, input)?; + input.expect_comma()?; + let second_point = Point::parse(context, input)?; + input.expect_comma()?; + let second_radius = Number::parse_non_negative(context, input)?; + + let (reverse_stops, point, radius) = if second_radius.value >= first_radius.value { + (false, second_point, second_radius) + } else { + (true, first_point, first_radius) + }; + + let rad = Circle::Radius(NonNegative(Length::from_px(radius.value))); + let shape = generic::EndingShape::Circle(rad); + let position: Position = point.into(); + let items = Gradient::parse_webkit_gradient_stops(context, input, reverse_stops)?; + + generic::Gradient::Radial { + shape, + position, + items, + repeating: false, + compat_mode: GradientCompatMode::Modern, + } + }, + _ => { + let e = SelectorParseErrorKind::UnexpectedIdent(ident.clone()); + return Err(input.new_custom_error(e)); + }, + }) + } + + fn parse_webkit_gradient_stops<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + reverse_stops: bool, + ) -> Result<LengthPercentageItemList, ParseError<'i>> { + let mut items = input + .try_parse(|i| { + i.expect_comma()?; + i.parse_comma_separated(|i| { + let function = i.expect_function()?.clone(); + let (color, mut p) = i.parse_nested_block(|i| { + let p = match_ignore_ascii_case! { &function, + "color-stop" => { + let p = NumberOrPercentage::parse(context, i)?.to_percentage(); + i.expect_comma()?; + p + }, + "from" => Percentage::zero(), + "to" => Percentage::hundred(), + _ => { + return Err(i.new_custom_error( + StyleParseErrorKind::UnexpectedFunction(function.clone()) + )) + }, + }; + let color = Color::parse(context, i)?; + if color == Color::CurrentColor { + return Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok((color.into(), p)) + })?; + if reverse_stops { + p.reverse(); + } + Ok(generic::GradientItem::ComplexColorStop { + color, + position: p.into(), + }) + }) + }) + .unwrap_or(vec![]); + + if items.is_empty() { + items = vec![ + generic::GradientItem::ComplexColorStop { + color: Color::transparent().into(), + position: Percentage::zero().into(), + }, + generic::GradientItem::ComplexColorStop { + color: Color::transparent().into(), + position: Percentage::hundred().into(), + }, + ]; + } else if items.len() == 1 { + let first = items[0].clone(); + items.push(first); + } else { + items.sort_by(|a, b| { + match (a, b) { + ( + &generic::GradientItem::ComplexColorStop { + position: ref a_position, + .. + }, + &generic::GradientItem::ComplexColorStop { + position: ref b_position, + .. + }, + ) => match (a_position, b_position) { + (&LengthPercentage::Percentage(a), &LengthPercentage::Percentage(b)) => { + return a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal); + }, + _ => {}, + }, + _ => {}, + } + if reverse_stops { + Ordering::Greater + } else { + Ordering::Less + } + }) + } + Ok(items.into()) + } + + /// Not used for -webkit-gradient syntax and conic-gradient + fn parse_stops<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<LengthPercentageItemList, ParseError<'i>> { + let items = + generic::GradientItem::parse_comma_separated(context, input, LengthPercentage::parse)?; + if items.len() < 2 { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(items) + } + + /// Parses a linear gradient. + /// GradientCompatMode can change during `-moz-` prefixed gradient parsing if it come across a `to` keyword. + fn parse_linear<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + repeating: bool, + mut compat_mode: GradientCompatMode, + ) -> Result<Self, ParseError<'i>> { + let direction = if let Ok(d) = + input.try_parse(|i| LineDirection::parse(context, i, &mut compat_mode)) + { + input.expect_comma()?; + d + } else { + match compat_mode { + GradientCompatMode::Modern => { + LineDirection::Vertical(VerticalPositionKeyword::Bottom) + }, + _ => LineDirection::Vertical(VerticalPositionKeyword::Top), + } + }; + let items = Gradient::parse_stops(context, input)?; + + Ok(Gradient::Linear { + direction, + items, + repeating, + compat_mode, + }) + } + + /// Parses a radial gradient. + fn parse_radial<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + repeating: bool, + compat_mode: GradientCompatMode, + ) -> Result<Self, ParseError<'i>> { + let (shape, position) = match compat_mode { + GradientCompatMode::Modern => { + let shape = input.try_parse(|i| EndingShape::parse(context, i, compat_mode)); + let position = input.try_parse(|i| { + i.expect_ident_matching("at")?; + Position::parse(context, i) + }); + (shape, position.ok()) + }, + _ => { + let position = input.try_parse(|i| Position::parse(context, i)); + let shape = input.try_parse(|i| { + if position.is_ok() { + i.expect_comma()?; + } + EndingShape::parse(context, i, compat_mode) + }); + (shape, position.ok()) + }, + }; + + if shape.is_ok() || position.is_some() { + input.expect_comma()?; + } + + let shape = shape.unwrap_or({ + generic::EndingShape::Ellipse(Ellipse::Extent(ShapeExtent::FarthestCorner)) + }); + + let position = position.unwrap_or(Position::center()); + + let items = Gradient::parse_stops(context, input)?; + + Ok(Gradient::Radial { + shape, + position, + items, + repeating, + compat_mode, + }) + } + fn parse_conic<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + repeating: bool, + ) -> Result<Self, ParseError<'i>> { + let angle = input.try_parse(|i| { + i.expect_ident_matching("from")?; + // Spec allows unitless zero start angles + // https://drafts.csswg.org/css-images-4/#valdef-conic-gradient-angle + Angle::parse_with_unitless(context, i) + }); + let position = input.try_parse(|i| { + i.expect_ident_matching("at")?; + Position::parse(context, i) + }); + if angle.is_ok() || position.is_ok() { + input.expect_comma()?; + } + + let angle = angle.unwrap_or(Angle::zero()); + let position = position.unwrap_or(Position::center()); + let items = generic::GradientItem::parse_comma_separated( + context, + input, + AngleOrPercentage::parse_with_unitless, + )?; + + if items.len() < 2 { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(Gradient::Conic { + angle, + position, + items, + repeating, + }) + } +} + +impl generic::LineDirection for LineDirection { + fn points_downwards(&self, compat_mode: GradientCompatMode) -> bool { + match *self { + LineDirection::Angle(ref angle) => angle.degrees() == 180.0, + LineDirection::Vertical(VerticalPositionKeyword::Bottom) => { + compat_mode == GradientCompatMode::Modern + }, + LineDirection::Vertical(VerticalPositionKeyword::Top) => { + compat_mode != GradientCompatMode::Modern + }, + _ => false, + } + } + + fn to_css<W>(&self, dest: &mut CssWriter<W>, compat_mode: GradientCompatMode) -> fmt::Result + where + W: Write, + { + match *self { + LineDirection::Angle(angle) => angle.to_css(dest), + LineDirection::Horizontal(x) => { + if compat_mode == GradientCompatMode::Modern { + dest.write_str("to ")?; + } + x.to_css(dest) + }, + LineDirection::Vertical(y) => { + if compat_mode == GradientCompatMode::Modern { + dest.write_str("to ")?; + } + y.to_css(dest) + }, + LineDirection::Corner(x, y) => { + if compat_mode == GradientCompatMode::Modern { + dest.write_str("to ")?; + } + x.to_css(dest)?; + dest.write_str(" ")?; + y.to_css(dest) + }, + } + } +} + +impl LineDirection { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + compat_mode: &mut GradientCompatMode, + ) -> Result<Self, ParseError<'i>> { + // Gradients allow unitless zero angles as an exception, see: + // https://github.com/w3c/csswg-drafts/issues/1162 + if let Ok(angle) = input.try_parse(|i| Angle::parse_with_unitless(context, i)) { + return Ok(LineDirection::Angle(angle)); + } + + input.try_parse(|i| { + let to_ident = i.try_parse(|i| i.expect_ident_matching("to")); + match *compat_mode { + // `to` keyword is mandatory in modern syntax. + GradientCompatMode::Modern => to_ident?, + // Fall back to Modern compatibility mode in case there is a `to` keyword. + // According to Gecko, `-moz-linear-gradient(to ...)` should serialize like + // `linear-gradient(to ...)`. + GradientCompatMode::Moz if to_ident.is_ok() => { + *compat_mode = GradientCompatMode::Modern + }, + // There is no `to` keyword in webkit prefixed syntax. If it's consumed, + // parsing should throw an error. + GradientCompatMode::WebKit if to_ident.is_ok() => { + return Err( + i.new_custom_error(SelectorParseErrorKind::UnexpectedIdent("to".into())) + ); + }, + _ => {}, + } + + if let Ok(x) = i.try_parse(HorizontalPositionKeyword::parse) { + if let Ok(y) = i.try_parse(VerticalPositionKeyword::parse) { + return Ok(LineDirection::Corner(x, y)); + } + return Ok(LineDirection::Horizontal(x)); + } + let y = VerticalPositionKeyword::parse(i)?; + if let Ok(x) = i.try_parse(HorizontalPositionKeyword::parse) { + return Ok(LineDirection::Corner(x, y)); + } + Ok(LineDirection::Vertical(y)) + }) + } +} + +impl EndingShape { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + compat_mode: GradientCompatMode, + ) -> Result<Self, ParseError<'i>> { + if let Ok(extent) = input.try_parse(|i| ShapeExtent::parse_with_compat_mode(i, compat_mode)) + { + if input + .try_parse(|i| i.expect_ident_matching("circle")) + .is_ok() + { + return Ok(generic::EndingShape::Circle(Circle::Extent(extent))); + } + let _ = input.try_parse(|i| i.expect_ident_matching("ellipse")); + return Ok(generic::EndingShape::Ellipse(Ellipse::Extent(extent))); + } + if input + .try_parse(|i| i.expect_ident_matching("circle")) + .is_ok() + { + if let Ok(extent) = + input.try_parse(|i| ShapeExtent::parse_with_compat_mode(i, compat_mode)) + { + return Ok(generic::EndingShape::Circle(Circle::Extent(extent))); + } + if compat_mode == GradientCompatMode::Modern { + if let Ok(length) = input.try_parse(|i| NonNegativeLength::parse(context, i)) { + return Ok(generic::EndingShape::Circle(Circle::Radius(length))); + } + } + return Ok(generic::EndingShape::Circle(Circle::Extent( + ShapeExtent::FarthestCorner, + ))); + } + if input + .try_parse(|i| i.expect_ident_matching("ellipse")) + .is_ok() + { + if let Ok(extent) = + input.try_parse(|i| ShapeExtent::parse_with_compat_mode(i, compat_mode)) + { + return Ok(generic::EndingShape::Ellipse(Ellipse::Extent(extent))); + } + if compat_mode == GradientCompatMode::Modern { + let pair: Result<_, ParseError> = input.try_parse(|i| { + let x = NonNegativeLengthPercentage::parse(context, i)?; + let y = NonNegativeLengthPercentage::parse(context, i)?; + Ok((x, y)) + }); + if let Ok((x, y)) = pair { + return Ok(generic::EndingShape::Ellipse(Ellipse::Radii(x, y))); + } + } + return Ok(generic::EndingShape::Ellipse(Ellipse::Extent( + ShapeExtent::FarthestCorner, + ))); + } + if let Ok(length) = input.try_parse(|i| NonNegativeLength::parse(context, i)) { + if let Ok(y) = input.try_parse(|i| NonNegativeLengthPercentage::parse(context, i)) { + if compat_mode == GradientCompatMode::Modern { + let _ = input.try_parse(|i| i.expect_ident_matching("ellipse")); + } + return Ok(generic::EndingShape::Ellipse(Ellipse::Radii( + NonNegative(LengthPercentage::from(length.0)), + y, + ))); + } + if compat_mode == GradientCompatMode::Modern { + let y = input.try_parse(|i| { + i.expect_ident_matching("ellipse")?; + NonNegativeLengthPercentage::parse(context, i) + }); + if let Ok(y) = y { + return Ok(generic::EndingShape::Ellipse(Ellipse::Radii( + NonNegative(LengthPercentage::from(length.0)), + y, + ))); + } + let _ = input.try_parse(|i| i.expect_ident_matching("circle")); + } + + return Ok(generic::EndingShape::Circle(Circle::Radius(length))); + } + input.try_parse(|i| { + let x = Percentage::parse_non_negative(context, i)?; + let y = if let Ok(y) = i.try_parse(|i| NonNegativeLengthPercentage::parse(context, i)) { + if compat_mode == GradientCompatMode::Modern { + let _ = i.try_parse(|i| i.expect_ident_matching("ellipse")); + } + y + } else { + if compat_mode == GradientCompatMode::Modern { + i.expect_ident_matching("ellipse")?; + } + NonNegativeLengthPercentage::parse(context, i)? + }; + Ok(generic::EndingShape::Ellipse(Ellipse::Radii( + NonNegative(LengthPercentage::from(x)), + y, + ))) + }) + } +} + +impl ShapeExtent { + fn parse_with_compat_mode<'i, 't>( + input: &mut Parser<'i, 't>, + compat_mode: GradientCompatMode, + ) -> Result<Self, ParseError<'i>> { + match Self::parse(input)? { + ShapeExtent::Contain | ShapeExtent::Cover + if compat_mode == GradientCompatMode::Modern => + { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + }, + ShapeExtent::Contain => Ok(ShapeExtent::ClosestSide), + ShapeExtent::Cover => Ok(ShapeExtent::FarthestCorner), + keyword => Ok(keyword), + } + } +} + +impl<T> generic::GradientItem<Color, T> { + fn parse_comma_separated<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + parse_position: impl for<'i1, 't1> Fn(&ParserContext, &mut Parser<'i1, 't1>) -> Result<T, ParseError<'i1>> + + Copy, + ) -> Result<crate::OwnedSlice<Self>, ParseError<'i>> { + let mut items = Vec::new(); + let mut seen_stop = false; + + loop { + input.parse_until_before(Delimiter::Comma, |input| { + if seen_stop { + if let Ok(hint) = input.try_parse(|i| parse_position(context, i)) { + seen_stop = false; + items.push(generic::GradientItem::InterpolationHint(hint)); + return Ok(()); + } + } + + let stop = generic::ColorStop::parse(context, input, parse_position)?; + + if let Ok(multi_position) = input.try_parse(|i| parse_position(context, i)) { + let stop_color = stop.color.clone(); + items.push(stop.into_item()); + items.push( + generic::ColorStop { + color: stop_color, + position: Some(multi_position), + } + .into_item(), + ); + } else { + items.push(stop.into_item()); + } + + seen_stop = true; + Ok(()) + })?; + + match input.next() { + Err(_) => break, + Ok(&Token::Comma) => continue, + Ok(_) => unreachable!(), + } + } + + if !seen_stop || items.len() < 2 { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(items.into()) + } +} + +impl<T> generic::ColorStop<Color, T> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + parse_position: impl for<'i1, 't1> Fn( + &ParserContext, + &mut Parser<'i1, 't1>, + ) -> Result<T, ParseError<'i1>>, + ) -> Result<Self, ParseError<'i>> { + Ok(generic::ColorStop { + color: Color::parse(context, input)?, + position: input.try_parse(|i| parse_position(context, i)).ok(), + }) + } +} + +impl Parse for PaintWorklet { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("paint")?; + input.parse_nested_block(|input| { + let name = Atom::from(&**input.expect_ident()?); + let arguments = input + .try_parse(|input| { + input.expect_comma()?; + input.parse_comma_separated(|input| SpecifiedValue::parse(input)) + }) + .unwrap_or(vec![]); + Ok(PaintWorklet { name, arguments }) + }) + } +} + +impl MozImageRect { + #[cfg(not(feature = "gecko"))] + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + _cors_mode: CorsMode, + ) -> Result<Self, ParseError<'i>> { + Err(input.new_error_for_next_token()) + } + + #[cfg(feature = "gecko")] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + ) -> Result<Self, ParseError<'i>> { + input.try_parse(|i| i.expect_function_matching("-moz-image-rect"))?; + input.parse_nested_block(|i| { + let string = i.expect_url_or_string()?; + let url = SpecifiedImageUrl::parse_from_string( + string.as_ref().to_owned(), + context, + cors_mode, + ); + i.expect_comma()?; + let top = NumberOrPercentage::parse_non_negative(context, i)?; + i.expect_comma()?; + let right = NumberOrPercentage::parse_non_negative(context, i)?; + i.expect_comma()?; + let bottom = NumberOrPercentage::parse_non_negative(context, i)?; + i.expect_comma()?; + let left = NumberOrPercentage::parse_non_negative(context, i)?; + Ok(MozImageRect { + url, + top, + right, + bottom, + left, + }) + }) + } +} diff --git a/servo/components/style/values/specified/length.rs b/servo/components/style/values/specified/length.rs new file mode 100644 index 0000000000..2eafa9fbd6 --- /dev/null +++ b/servo/components/style/values/specified/length.rs @@ -0,0 +1,1285 @@ +/* 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/. */ + +//! [Length values][length]. +//! +//! [length]: https://drafts.csswg.org/css-values/#lengths + +use super::{AllowQuirks, Number, Percentage, ToComputedValue}; +use crate::computed_value_flags::ComputedValueFlags; +use crate::font_metrics::{FontMetrics, FontMetricsOrientation}; +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::{self, CSSPixelLength, Context}; +use crate::values::generics::length as generics; +use crate::values::generics::length::{ + GenericLengthOrNumber, GenericLengthPercentageOrNormal, GenericMaxSize, GenericSize, +}; +use crate::values::generics::NonNegative; +use crate::values::specified::calc::{self, CalcNode}; +use crate::values::specified::NonNegativeNumber; +use crate::values::CSSFloat; +use crate::Zero; +use app_units::Au; +use cssparser::{Parser, Token}; +use euclid::default::Size2D; +use std::cmp; +use std::ops::{Add, Mul}; +use style_traits::values::specified::AllowedNumericType; +use style_traits::{ParseError, SpecifiedValueInfo, StyleParseErrorKind}; + +pub use super::image::Image; +pub use super::image::{EndingShape as GradientEndingShape, Gradient}; +pub use crate::values::specified::calc::CalcLengthPercentage; + +/// Number of app units per pixel +pub const AU_PER_PX: CSSFloat = 60.; +/// Number of app units per inch +pub const AU_PER_IN: CSSFloat = AU_PER_PX * 96.; +/// Number of app units per centimeter +pub const AU_PER_CM: CSSFloat = AU_PER_IN / 2.54; +/// Number of app units per millimeter +pub const AU_PER_MM: CSSFloat = AU_PER_IN / 25.4; +/// Number of app units per quarter +pub const AU_PER_Q: CSSFloat = AU_PER_MM / 4.; +/// Number of app units per point +pub const AU_PER_PT: CSSFloat = AU_PER_IN / 72.; +/// Number of app units per pica +pub const AU_PER_PC: CSSFloat = AU_PER_PT * 12.; + +/// A font relative length. +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)] +pub enum FontRelativeLength { + /// A "em" value: https://drafts.csswg.org/css-values/#em + #[css(dimension)] + Em(CSSFloat), + /// A "ex" value: https://drafts.csswg.org/css-values/#ex + #[css(dimension)] + Ex(CSSFloat), + /// A "ch" value: https://drafts.csswg.org/css-values/#ch + #[css(dimension)] + Ch(CSSFloat), + /// A "rem" value: https://drafts.csswg.org/css-values/#rem + #[css(dimension)] + Rem(CSSFloat), +} + +/// A source to resolve font-relative units against +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum FontBaseSize { + /// Use the font-size of the current element. + CurrentStyle, + /// Use the inherited font-size. + InheritedStyle, +} + +impl FontBaseSize { + /// Calculate the actual size for a given context + pub fn resolve(&self, context: &Context) -> computed::Length { + match *self { + FontBaseSize::CurrentStyle => context.style().get_font().clone_font_size().size(), + FontBaseSize::InheritedStyle => { + context.style().get_parent_font().clone_font_size().size() + }, + } + } +} + +impl FontRelativeLength { + /// Return true if this is a zero value. + fn is_zero(&self) -> bool { + match *self { + FontRelativeLength::Em(v) | + FontRelativeLength::Ex(v) | + FontRelativeLength::Ch(v) | + FontRelativeLength::Rem(v) => v == 0., + } + } + + fn is_negative(&self) -> bool { + match *self { + FontRelativeLength::Em(v) | + FontRelativeLength::Ex(v) | + FontRelativeLength::Ch(v) | + FontRelativeLength::Rem(v) => v < 0., + } + } + + fn try_sum(&self, other: &Self) -> Result<Self, ()> { + use self::FontRelativeLength::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return Err(()); + } + + Ok(match (self, other) { + (&Em(one), &Em(other)) => Em(one + other), + (&Ex(one), &Ex(other)) => Ex(one + other), + (&Ch(one), &Ch(other)) => Ch(one + other), + (&Rem(one), &Rem(other)) => Rem(one + other), + // See https://github.com/rust-lang/rust/issues/68867. rustc isn't + // able to figure it own on its own so we help. + _ => unsafe { + match *self { + Em(..) | Ex(..) | Ch(..) | Rem(..) => {}, + } + debug_unreachable!("Forgot to handle unit in try_sum()") + }, + }) + } + + /// Computes the font-relative length. + pub fn to_computed_value( + &self, + context: &Context, + base_size: FontBaseSize, + ) -> computed::Length { + let (reference_size, length) = self.reference_font_size_and_length(context, base_size); + (reference_size * length).normalized() + } + + /// Return reference font size. + /// + /// We use the base_size flag to pass a different size for computing + /// font-size and unconstrained font-size. + /// + /// This returns a pair, the first one is the reference font size, and the + /// second one is the unpacked relative length. + fn reference_font_size_and_length( + &self, + context: &Context, + base_size: FontBaseSize, + ) -> (computed::Length, CSSFloat) { + fn query_font_metrics( + context: &Context, + base_size: FontBaseSize, + orientation: FontMetricsOrientation, + ) -> FontMetrics { + context + .font_metrics_provider + .query(context, base_size, orientation) + } + + let reference_font_size = base_size.resolve(context); + let font_metrics_flag = match base_size { + FontBaseSize::CurrentStyle => ComputedValueFlags::DEPENDS_ON_SELF_FONT_METRICS, + FontBaseSize::InheritedStyle => ComputedValueFlags::DEPENDS_ON_INHERITED_FONT_METRICS, + }; + match *self { + FontRelativeLength::Em(length) => { + if context.for_non_inherited_property.is_some() { + if base_size == FontBaseSize::CurrentStyle { + context + .rule_cache_conditions + .borrow_mut() + .set_font_size_dependency(reference_font_size.into()); + } + } + + (reference_font_size, length) + }, + FontRelativeLength::Ex(length) => { + if context.for_non_inherited_property.is_some() { + context.rule_cache_conditions.borrow_mut().set_uncacheable(); + } + context.builder.add_flags(font_metrics_flag); + // The x-height is an intrinsically horizontal metric. + let metrics = + query_font_metrics(context, base_size, FontMetricsOrientation::Horizontal); + let reference_size = metrics.x_height.unwrap_or_else(|| { + // https://drafts.csswg.org/css-values/#ex + // + // In the cases where it is impossible or impractical to + // determine the x-height, a value of 0.5em must be + // assumed. + // + reference_font_size * 0.5 + }); + (reference_size, length) + }, + FontRelativeLength::Ch(length) => { + if context.for_non_inherited_property.is_some() { + context.rule_cache_conditions.borrow_mut().set_uncacheable(); + } + context.builder.add_flags(font_metrics_flag); + // https://drafts.csswg.org/css-values/#ch: + // + // Equal to the used advance measure of the “0” (ZERO, + // U+0030) glyph in the font used to render it. (The advance + // measure of a glyph is its advance width or height, + // whichever is in the inline axis of the element.) + // + let metrics = + query_font_metrics(context, base_size, FontMetricsOrientation::MatchContext); + let reference_size = metrics.zero_advance_measure.unwrap_or_else(|| { + // https://drafts.csswg.org/css-values/#ch + // + // In the cases where it is impossible or impractical to + // determine the measure of the “0” glyph, it must be + // assumed to be 0.5em wide by 1em tall. Thus, the ch + // unit falls back to 0.5em in the general case, and to + // 1em when it would be typeset upright (i.e. + // writing-mode is vertical-rl or vertical-lr and + // text-orientation is upright). + // + let wm = context.style().writing_mode; + if wm.is_vertical() && wm.is_upright() { + reference_font_size + } else { + reference_font_size * 0.5 + } + }); + (reference_size, length) + }, + FontRelativeLength::Rem(length) => { + // https://drafts.csswg.org/css-values/#rem: + // + // When specified on the font-size property of the root + // element, the rem units refer to the property's initial + // value. + // + let reference_size = if context.builder.is_root_element || context.in_media_query { + reference_font_size + } else { + context.device().root_font_size() + }; + (reference_size, length) + }, + } + } +} + +/// A viewport-relative length. +/// +/// <https://drafts.csswg.org/css-values/#viewport-relative-lengths> +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)] +pub enum ViewportPercentageLength { + /// A vw unit: https://drafts.csswg.org/css-values/#vw + #[css(dimension)] + Vw(CSSFloat), + /// A vh unit: https://drafts.csswg.org/css-values/#vh + #[css(dimension)] + Vh(CSSFloat), + /// <https://drafts.csswg.org/css-values/#vmin> + #[css(dimension)] + Vmin(CSSFloat), + /// <https://drafts.csswg.org/css-values/#vmax> + #[css(dimension)] + Vmax(CSSFloat), +} + +impl ViewportPercentageLength { + /// Return true if this is a zero value. + fn is_zero(&self) -> bool { + match *self { + ViewportPercentageLength::Vw(v) | + ViewportPercentageLength::Vh(v) | + ViewportPercentageLength::Vmin(v) | + ViewportPercentageLength::Vmax(v) => v == 0., + } + } + + fn is_negative(&self) -> bool { + match *self { + ViewportPercentageLength::Vw(v) | + ViewportPercentageLength::Vh(v) | + ViewportPercentageLength::Vmin(v) | + ViewportPercentageLength::Vmax(v) => v < 0., + } + } + + fn try_sum(&self, other: &Self) -> Result<Self, ()> { + use self::ViewportPercentageLength::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return Err(()); + } + + Ok(match (self, other) { + (&Vw(one), &Vw(other)) => Vw(one + other), + (&Vh(one), &Vh(other)) => Vh(one + other), + (&Vmin(one), &Vmin(other)) => Vmin(one + other), + (&Vmax(one), &Vmax(other)) => Vmax(one + other), + // See https://github.com/rust-lang/rust/issues/68867. rustc isn't + // able to figure it own on its own so we help. + _ => unsafe { + match *self { + Vw(..) | Vh(..) | Vmin(..) | Vmax(..) => {}, + } + debug_unreachable!("Forgot to handle unit in try_sum()") + }, + }) + } + + /// Computes the given viewport-relative length for the given viewport size. + pub fn to_computed_value(&self, viewport_size: Size2D<Au>) -> CSSPixelLength { + let (factor, length) = match *self { + ViewportPercentageLength::Vw(length) => (length, viewport_size.width), + ViewportPercentageLength::Vh(length) => (length, viewport_size.height), + ViewportPercentageLength::Vmin(length) => { + (length, cmp::min(viewport_size.width, viewport_size.height)) + }, + ViewportPercentageLength::Vmax(length) => { + (length, cmp::max(viewport_size.width, viewport_size.height)) + }, + }; + + // FIXME: Bug 1396535, we need to fix the extremely small viewport length for transform. + // See bug 989802. We truncate so that adding multiple viewport units + // that add up to 100 does not overflow due to rounding differences + let trunc_scaled = ((length.0 as f64) * factor as f64 / 100.).trunc(); + Au::from_f64_au(trunc_scaled).into() + } +} + +/// HTML5 "character width", as defined in HTML5 § 14.5.4. +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)] +pub struct CharacterWidth(pub i32); + +impl CharacterWidth { + /// Computes the given character width. + pub fn to_computed_value(&self, reference_font_size: computed::Length) -> computed::Length { + // This applies the *converting a character width to pixels* algorithm + // as specified in HTML5 § 14.5.4. + // + // TODO(pcwalton): Find these from the font. + let average_advance = reference_font_size * 0.5; + let max_advance = reference_font_size; + average_advance * (self.0 as CSSFloat - 1.0) + max_advance + } +} + +/// Represents an absolute length with its unit +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)] +pub enum AbsoluteLength { + /// An absolute length in pixels (px) + #[css(dimension)] + Px(CSSFloat), + /// An absolute length in inches (in) + #[css(dimension)] + In(CSSFloat), + /// An absolute length in centimeters (cm) + #[css(dimension)] + Cm(CSSFloat), + /// An absolute length in millimeters (mm) + #[css(dimension)] + Mm(CSSFloat), + /// An absolute length in quarter-millimeters (q) + #[css(dimension)] + Q(CSSFloat), + /// An absolute length in points (pt) + #[css(dimension)] + Pt(CSSFloat), + /// An absolute length in pica (pc) + #[css(dimension)] + Pc(CSSFloat), +} + +impl AbsoluteLength { + fn is_zero(&self) -> bool { + match *self { + AbsoluteLength::Px(v) | + AbsoluteLength::In(v) | + AbsoluteLength::Cm(v) | + AbsoluteLength::Mm(v) | + AbsoluteLength::Q(v) | + AbsoluteLength::Pt(v) | + AbsoluteLength::Pc(v) => v == 0., + } + } + + fn is_negative(&self) -> bool { + match *self { + AbsoluteLength::Px(v) | + AbsoluteLength::In(v) | + AbsoluteLength::Cm(v) | + AbsoluteLength::Mm(v) | + AbsoluteLength::Q(v) | + AbsoluteLength::Pt(v) | + AbsoluteLength::Pc(v) => v < 0., + } + } + + /// Convert this into a pixel value. + #[inline] + pub fn to_px(&self) -> CSSFloat { + use std::f32; + + let pixel = match *self { + AbsoluteLength::Px(value) => value, + AbsoluteLength::In(value) => value * (AU_PER_IN / AU_PER_PX), + AbsoluteLength::Cm(value) => value * (AU_PER_CM / AU_PER_PX), + AbsoluteLength::Mm(value) => value * (AU_PER_MM / AU_PER_PX), + AbsoluteLength::Q(value) => value * (AU_PER_Q / AU_PER_PX), + AbsoluteLength::Pt(value) => value * (AU_PER_PT / AU_PER_PX), + AbsoluteLength::Pc(value) => value * (AU_PER_PC / AU_PER_PX), + }; + pixel.min(f32::MAX).max(f32::MIN) + } +} + +impl ToComputedValue for AbsoluteLength { + type ComputedValue = CSSPixelLength; + + fn to_computed_value(&self, _: &Context) -> Self::ComputedValue { + CSSPixelLength::new(self.to_px()) + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + AbsoluteLength::Px(computed.px()) + } +} + +impl PartialOrd for AbsoluteLength { + fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> { + self.to_px().partial_cmp(&other.to_px()) + } +} + +impl Mul<CSSFloat> for AbsoluteLength { + type Output = AbsoluteLength; + + #[inline] + fn mul(self, scalar: CSSFloat) -> AbsoluteLength { + match self { + AbsoluteLength::Px(v) => AbsoluteLength::Px(v * scalar), + AbsoluteLength::In(v) => AbsoluteLength::In(v * scalar), + AbsoluteLength::Cm(v) => AbsoluteLength::Cm(v * scalar), + AbsoluteLength::Mm(v) => AbsoluteLength::Mm(v * scalar), + AbsoluteLength::Q(v) => AbsoluteLength::Q(v * scalar), + AbsoluteLength::Pt(v) => AbsoluteLength::Pt(v * scalar), + AbsoluteLength::Pc(v) => AbsoluteLength::Pc(v * scalar), + } + } +} + +impl Add<AbsoluteLength> for AbsoluteLength { + type Output = Self; + + #[inline] + fn add(self, rhs: Self) -> Self { + match (self, rhs) { + (AbsoluteLength::Px(x), AbsoluteLength::Px(y)) => AbsoluteLength::Px(x + y), + (AbsoluteLength::In(x), AbsoluteLength::In(y)) => AbsoluteLength::In(x + y), + (AbsoluteLength::Cm(x), AbsoluteLength::Cm(y)) => AbsoluteLength::Cm(x + y), + (AbsoluteLength::Mm(x), AbsoluteLength::Mm(y)) => AbsoluteLength::Mm(x + y), + (AbsoluteLength::Q(x), AbsoluteLength::Q(y)) => AbsoluteLength::Q(x + y), + (AbsoluteLength::Pt(x), AbsoluteLength::Pt(y)) => AbsoluteLength::Pt(x + y), + (AbsoluteLength::Pc(x), AbsoluteLength::Pc(y)) => AbsoluteLength::Pc(x + y), + _ => AbsoluteLength::Px(self.to_px() + rhs.to_px()), + } + } +} + +/// A `<length>` without taking `calc` expressions into account +/// +/// <https://drafts.csswg.org/css-values/#lengths> +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)] +pub enum NoCalcLength { + /// An absolute length + /// + /// <https://drafts.csswg.org/css-values/#absolute-length> + Absolute(AbsoluteLength), + + /// A font-relative length: + /// + /// <https://drafts.csswg.org/css-values/#font-relative-lengths> + FontRelative(FontRelativeLength), + + /// A viewport-relative length. + /// + /// <https://drafts.csswg.org/css-values/#viewport-relative-lengths> + ViewportPercentage(ViewportPercentageLength), + + /// HTML5 "character width", as defined in HTML5 § 14.5.4. + /// + /// This cannot be specified by the user directly and is only generated by + /// `Stylist::synthesize_rules_for_legacy_attributes()`. + #[css(function)] + ServoCharacterWidth(CharacterWidth), +} + +impl Mul<CSSFloat> for NoCalcLength { + type Output = NoCalcLength; + + #[inline] + fn mul(self, scalar: CSSFloat) -> NoCalcLength { + match self { + NoCalcLength::Absolute(v) => NoCalcLength::Absolute(v * scalar), + NoCalcLength::FontRelative(v) => NoCalcLength::FontRelative(v * scalar), + NoCalcLength::ViewportPercentage(v) => NoCalcLength::ViewportPercentage(v * scalar), + NoCalcLength::ServoCharacterWidth(_) => panic!("Can't multiply ServoCharacterWidth!"), + } + } +} + +impl NoCalcLength { + /// Returns whether the value of this length without unit is less than zero. + pub fn is_negative(&self) -> bool { + match *self { + NoCalcLength::Absolute(v) => v.is_negative(), + NoCalcLength::FontRelative(v) => v.is_negative(), + NoCalcLength::ViewportPercentage(v) => v.is_negative(), + NoCalcLength::ServoCharacterWidth(c) => c.0 < 0, + } + } + + /// Parse a given absolute or relative dimension. + pub fn parse_dimension( + context: &ParserContext, + value: CSSFloat, + unit: &str, + ) -> Result<Self, ()> { + Ok(match_ignore_ascii_case! { unit, + "px" => NoCalcLength::Absolute(AbsoluteLength::Px(value)), + "in" => NoCalcLength::Absolute(AbsoluteLength::In(value)), + "cm" => NoCalcLength::Absolute(AbsoluteLength::Cm(value)), + "mm" => NoCalcLength::Absolute(AbsoluteLength::Mm(value)), + "q" => NoCalcLength::Absolute(AbsoluteLength::Q(value)), + "pt" => NoCalcLength::Absolute(AbsoluteLength::Pt(value)), + "pc" => NoCalcLength::Absolute(AbsoluteLength::Pc(value)), + // font-relative + "em" => NoCalcLength::FontRelative(FontRelativeLength::Em(value)), + "ex" => NoCalcLength::FontRelative(FontRelativeLength::Ex(value)), + "ch" => NoCalcLength::FontRelative(FontRelativeLength::Ch(value)), + "rem" => NoCalcLength::FontRelative(FontRelativeLength::Rem(value)), + // viewport percentages + "vw" if !context.in_page_rule() => { + NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vw(value)) + }, + "vh" if !context.in_page_rule() => { + NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vh(value)) + }, + "vmin" if !context.in_page_rule() => { + NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vmin(value)) + }, + "vmax" if !context.in_page_rule() => { + NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vmax(value)) + }, + _ => return Err(()), + }) + } + + /// Try to sume two lengths if compatible into the left hand side. + pub(crate) fn try_sum(&self, other: &Self) -> Result<Self, ()> { + use self::NoCalcLength::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return Err(()); + } + + Ok(match (self, other) { + (&Absolute(ref one), &Absolute(ref other)) => Absolute(*one + *other), + (&FontRelative(ref one), &FontRelative(ref other)) => FontRelative(one.try_sum(other)?), + (&ViewportPercentage(ref one), &ViewportPercentage(ref other)) => { + ViewportPercentage(one.try_sum(other)?) + }, + (&ServoCharacterWidth(ref one), &ServoCharacterWidth(ref other)) => { + ServoCharacterWidth(CharacterWidth(one.0 + other.0)) + }, + // See https://github.com/rust-lang/rust/issues/68867. rustc isn't + // able to figure it own on its own so we help. + _ => unsafe { + match *self { + Absolute(..) | + FontRelative(..) | + ViewportPercentage(..) | + ServoCharacterWidth(..) => {}, + } + debug_unreachable!("Forgot to handle unit in try_sum()") + }, + }) + } + + /// Get a px value without context. + #[inline] + pub fn to_computed_pixel_length_without_context(&self) -> Result<CSSFloat, ()> { + match *self { + NoCalcLength::Absolute(len) => Ok(len.to_px()), + _ => Err(()), + } + } + + /// Get an absolute length from a px value. + #[inline] + pub fn from_px(px_value: CSSFloat) -> NoCalcLength { + NoCalcLength::Absolute(AbsoluteLength::Px(px_value)) + } +} + +impl SpecifiedValueInfo for NoCalcLength {} + +impl PartialOrd for NoCalcLength { + fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> { + use self::NoCalcLength::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return None; + } + + match (self, other) { + (&Absolute(ref one), &Absolute(ref other)) => one.to_px().partial_cmp(&other.to_px()), + (&FontRelative(ref one), &FontRelative(ref other)) => one.partial_cmp(other), + (&ViewportPercentage(ref one), &ViewportPercentage(ref other)) => { + one.partial_cmp(other) + }, + (&ServoCharacterWidth(ref one), &ServoCharacterWidth(ref other)) => { + one.0.partial_cmp(&other.0) + }, + // See https://github.com/rust-lang/rust/issues/68867. rustc isn't + // able to figure it own on its own so we help. + _ => unsafe { + match *self { + Absolute(..) | + FontRelative(..) | + ViewportPercentage(..) | + ServoCharacterWidth(..) => {}, + } + debug_unreachable!("Forgot an arm in partial_cmp?") + }, + } + } +} + +impl Zero for NoCalcLength { + fn zero() -> Self { + NoCalcLength::Absolute(AbsoluteLength::Px(0.)) + } + + fn is_zero(&self) -> bool { + match *self { + NoCalcLength::Absolute(v) => v.is_zero(), + NoCalcLength::FontRelative(v) => v.is_zero(), + NoCalcLength::ViewportPercentage(v) => v.is_zero(), + NoCalcLength::ServoCharacterWidth(v) => v.0 == 0, + } + } +} + +/// An extension to `NoCalcLength` to parse `calc` expressions. +/// This is commonly used for the `<length>` values. +/// +/// <https://drafts.csswg.org/css-values/#lengths> +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum Length { + /// The internal length type that cannot parse `calc` + NoCalc(NoCalcLength), + /// A calc expression. + /// + /// <https://drafts.csswg.org/css-values/#calc-notation> + Calc(Box<CalcLengthPercentage>), +} + +impl From<NoCalcLength> for Length { + #[inline] + fn from(len: NoCalcLength) -> Self { + Length::NoCalc(len) + } +} + +impl Mul<CSSFloat> for Length { + type Output = Length; + + #[inline] + fn mul(self, scalar: CSSFloat) -> Length { + match self { + Length::NoCalc(inner) => Length::NoCalc(inner * scalar), + Length::Calc(..) => panic!("Can't multiply Calc!"), + } + } +} + +impl PartialOrd for FontRelativeLength { + fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> { + use self::FontRelativeLength::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return None; + } + + match (self, other) { + (&Em(ref one), &Em(ref other)) => one.partial_cmp(other), + (&Ex(ref one), &Ex(ref other)) => one.partial_cmp(other), + (&Ch(ref one), &Ch(ref other)) => one.partial_cmp(other), + (&Rem(ref one), &Rem(ref other)) => one.partial_cmp(other), + // See https://github.com/rust-lang/rust/issues/68867. rustc isn't + // able to figure it own on its own so we help. + _ => unsafe { + match *self { + Em(..) | Ex(..) | Ch(..) | Rem(..) => {}, + } + debug_unreachable!("Forgot an arm in partial_cmp?") + }, + } + } +} + +impl Mul<CSSFloat> for FontRelativeLength { + type Output = FontRelativeLength; + + #[inline] + fn mul(self, scalar: CSSFloat) -> FontRelativeLength { + match self { + FontRelativeLength::Em(v) => FontRelativeLength::Em(v * scalar), + FontRelativeLength::Ex(v) => FontRelativeLength::Ex(v * scalar), + FontRelativeLength::Ch(v) => FontRelativeLength::Ch(v * scalar), + FontRelativeLength::Rem(v) => FontRelativeLength::Rem(v * scalar), + } + } +} + +impl Mul<CSSFloat> for ViewportPercentageLength { + type Output = ViewportPercentageLength; + + #[inline] + fn mul(self, scalar: CSSFloat) -> ViewportPercentageLength { + match self { + ViewportPercentageLength::Vw(v) => ViewportPercentageLength::Vw(v * scalar), + ViewportPercentageLength::Vh(v) => ViewportPercentageLength::Vh(v * scalar), + ViewportPercentageLength::Vmin(v) => ViewportPercentageLength::Vmin(v * scalar), + ViewportPercentageLength::Vmax(v) => ViewportPercentageLength::Vmax(v * scalar), + } + } +} + +impl PartialOrd for ViewportPercentageLength { + fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> { + use self::ViewportPercentageLength::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return None; + } + + match (self, other) { + (&Vw(ref one), &Vw(ref other)) => one.partial_cmp(other), + (&Vh(ref one), &Vh(ref other)) => one.partial_cmp(other), + (&Vmin(ref one), &Vmin(ref other)) => one.partial_cmp(other), + (&Vmax(ref one), &Vmax(ref other)) => one.partial_cmp(other), + // See https://github.com/rust-lang/rust/issues/68867. rustc isn't + // able to figure it own on its own so we help. + _ => unsafe { + match *self { + Vw(..) | Vh(..) | Vmin(..) | Vmax(..) => {}, + } + debug_unreachable!("Forgot an arm in partial_cmp?") + }, + } + } +} + +impl Length { + #[inline] + fn parse_internal<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + num_context: AllowedNumericType, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let token = input.next()?; + match *token { + Token::Dimension { + value, ref unit, .. + } if num_context.is_ok(context.parsing_mode, value) => { + NoCalcLength::parse_dimension(context, value, unit) + .map(Length::NoCalc) + .map_err(|()| location.new_unexpected_token_error(token.clone())) + }, + Token::Number { value, .. } if num_context.is_ok(context.parsing_mode, value) => { + if value != 0. && + !context.parsing_mode.allows_unitless_lengths() && + !allow_quirks.allowed(context.quirks_mode) + { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(Length::NoCalc(NoCalcLength::Absolute(AbsoluteLength::Px( + value, + )))) + }, + Token::Function(ref name) => { + let function = CalcNode::math_function(name, location)?; + let calc = CalcNode::parse_length(context, input, num_context, function)?; + Ok(Length::Calc(Box::new(calc))) + }, + ref token => return Err(location.new_unexpected_token_error(token.clone())), + } + } + + /// Parse a non-negative length + #[inline] + pub fn parse_non_negative<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_non_negative_quirky(context, input, AllowQuirks::No) + } + + /// Parse a non-negative length, allowing quirks. + #[inline] + pub fn parse_non_negative_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal( + context, + input, + AllowedNumericType::NonNegative, + allow_quirks, + ) + } + + /// Get an absolute length from a px value. + #[inline] + pub fn from_px(px_value: CSSFloat) -> Length { + Length::NoCalc(NoCalcLength::from_px(px_value)) + } +} + +impl Parse for Length { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_quirky(context, input, AllowQuirks::No) + } +} + +impl Zero for Length { + fn zero() -> Self { + Length::NoCalc(NoCalcLength::zero()) + } + + fn is_zero(&self) -> bool { + // FIXME(emilio): Seems a bit weird to treat calc() unconditionally as + // non-zero here? + match *self { + Length::NoCalc(ref l) => l.is_zero(), + Length::Calc(..) => false, + } + } +} + +impl Length { + /// Parses a length, with quirks. + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal(context, input, AllowedNumericType::All, allow_quirks) + } +} + +/// A wrapper of Length, whose value must be >= 0. +pub type NonNegativeLength = NonNegative<Length>; + +impl Parse for NonNegativeLength { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(NonNegative(Length::parse_non_negative(context, input)?)) + } +} + +impl From<NoCalcLength> for NonNegativeLength { + #[inline] + fn from(len: NoCalcLength) -> Self { + NonNegative(Length::NoCalc(len)) + } +} + +impl From<Length> for NonNegativeLength { + #[inline] + fn from(len: Length) -> Self { + NonNegative(len) + } +} + +impl NonNegativeLength { + /// Get an absolute length from a px value. + #[inline] + pub fn from_px(px_value: CSSFloat) -> Self { + Length::from_px(px_value.max(0.)).into() + } + + /// Parses a non-negative length, optionally with quirks. + #[inline] + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + Ok(NonNegative(Length::parse_non_negative_quirky( + context, + input, + allow_quirks, + )?)) + } +} + +/// A `<length-percentage>` value. This can be either a `<length>`, a +/// `<percentage>`, or a combination of both via `calc()`. +/// +/// https://drafts.csswg.org/css-values-4/#typedef-length-percentage +#[allow(missing_docs)] +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum LengthPercentage { + Length(NoCalcLength), + Percentage(computed::Percentage), + Calc(Box<CalcLengthPercentage>), +} + +impl From<Length> for LengthPercentage { + fn from(len: Length) -> LengthPercentage { + match len { + Length::NoCalc(l) => LengthPercentage::Length(l), + Length::Calc(l) => LengthPercentage::Calc(l), + } + } +} + +impl From<NoCalcLength> for LengthPercentage { + #[inline] + fn from(len: NoCalcLength) -> Self { + LengthPercentage::Length(len) + } +} + +impl From<Percentage> for LengthPercentage { + #[inline] + fn from(pc: Percentage) -> Self { + if pc.is_calc() { + // FIXME(emilio): Hard-coding the clamping mode is suspect. + LengthPercentage::Calc(Box::new(CalcLengthPercentage { + clamping_mode: AllowedNumericType::All, + node: CalcNode::Leaf(calc::Leaf::Percentage(pc.get())), + })) + } else { + LengthPercentage::Percentage(computed::Percentage(pc.get())) + } + } +} + +impl From<computed::Percentage> for LengthPercentage { + #[inline] + fn from(pc: computed::Percentage) -> Self { + LengthPercentage::Percentage(pc) + } +} + +impl Parse for LengthPercentage { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_quirky(context, input, AllowQuirks::No) + } +} + +impl LengthPercentage { + #[inline] + /// Returns a `0%` value. + pub fn zero_percent() -> LengthPercentage { + LengthPercentage::Percentage(computed::Percentage::zero()) + } + + fn parse_internal<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + num_context: AllowedNumericType, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let token = input.next()?; + match *token { + Token::Dimension { + value, ref unit, .. + } if num_context.is_ok(context.parsing_mode, value) => { + return NoCalcLength::parse_dimension(context, value, unit) + .map(LengthPercentage::Length) + .map_err(|()| location.new_unexpected_token_error(token.clone())); + }, + Token::Percentage { unit_value, .. } + if num_context.is_ok(context.parsing_mode, unit_value) => + { + return Ok(LengthPercentage::Percentage(computed::Percentage( + unit_value, + ))); + } + Token::Number { value, .. } if num_context.is_ok(context.parsing_mode, value) => { + if value != 0. && + !context.parsing_mode.allows_unitless_lengths() && + !allow_quirks.allowed(context.quirks_mode) + { + return Err(location.new_unexpected_token_error(token.clone())); + } else { + return Ok(LengthPercentage::Length(NoCalcLength::from_px(value))); + } + }, + Token::Function(ref name) => { + let function = CalcNode::math_function(name, location)?; + let calc = + CalcNode::parse_length_or_percentage(context, input, num_context, function)?; + Ok(LengthPercentage::Calc(Box::new(calc))) + }, + _ => return Err(location.new_unexpected_token_error(token.clone())), + } + } + + /// Parses allowing the unitless length quirk. + /// <https://quirks.spec.whatwg.org/#the-unitless-length-quirk> + #[inline] + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal(context, input, AllowedNumericType::All, allow_quirks) + } + + /// Parse a non-negative length. + /// + /// FIXME(emilio): This should be not public and we should use + /// NonNegativeLengthPercentage instead. + #[inline] + pub fn parse_non_negative<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_non_negative_quirky(context, input, AllowQuirks::No) + } + + /// Parse a non-negative length, with quirks. + #[inline] + pub fn parse_non_negative_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal( + context, + input, + AllowedNumericType::NonNegative, + allow_quirks, + ) + } +} + +impl Zero for LengthPercentage { + fn zero() -> Self { + LengthPercentage::Length(NoCalcLength::zero()) + } + + fn is_zero(&self) -> bool { + match *self { + LengthPercentage::Length(l) => l.is_zero(), + LengthPercentage::Percentage(p) => p.0 == 0.0, + LengthPercentage::Calc(_) => false, + } + } +} + +/// A specified type for `<length-percentage> | auto`. +pub type LengthPercentageOrAuto = generics::LengthPercentageOrAuto<LengthPercentage>; + +impl LengthPercentageOrAuto { + /// Returns a value representing `0%`. + #[inline] + pub fn zero_percent() -> Self { + generics::LengthPercentageOrAuto::LengthPercentage(LengthPercentage::zero_percent()) + } + + /// Parses a length or a percentage, allowing the unitless length quirk. + /// <https://quirks.spec.whatwg.org/#the-unitless-length-quirk> + #[inline] + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with(context, input, |context, input| { + LengthPercentage::parse_quirky(context, input, allow_quirks) + }) + } +} + +/// A wrapper of LengthPercentageOrAuto, whose value must be >= 0. +pub type NonNegativeLengthPercentageOrAuto = + generics::LengthPercentageOrAuto<NonNegativeLengthPercentage>; + +impl NonNegativeLengthPercentageOrAuto { + /// Returns a value representing `0%`. + #[inline] + pub fn zero_percent() -> Self { + generics::LengthPercentageOrAuto::LengthPercentage( + NonNegativeLengthPercentage::zero_percent(), + ) + } + + /// Parses a non-negative length-percentage, allowing the unitless length + /// quirk. + #[inline] + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with(context, input, |context, input| { + NonNegativeLengthPercentage::parse_quirky(context, input, allow_quirks) + }) + } +} + +/// A wrapper of LengthPercentage, whose value must be >= 0. +pub type NonNegativeLengthPercentage = NonNegative<LengthPercentage>; + +/// Either a NonNegativeLengthPercentage or the `normal` keyword. +pub type NonNegativeLengthPercentageOrNormal = + GenericLengthPercentageOrNormal<NonNegativeLengthPercentage>; + +impl From<NoCalcLength> for NonNegativeLengthPercentage { + #[inline] + fn from(len: NoCalcLength) -> Self { + NonNegative(LengthPercentage::from(len)) + } +} + +impl Parse for NonNegativeLengthPercentage { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_quirky(context, input, AllowQuirks::No) + } +} + +impl NonNegativeLengthPercentage { + #[inline] + /// Returns a `0%` value. + pub fn zero_percent() -> Self { + NonNegative(LengthPercentage::zero_percent()) + } + + /// Parses a length or a percentage, allowing the unitless length quirk. + /// <https://quirks.spec.whatwg.org/#the-unitless-length-quirk> + #[inline] + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + LengthPercentage::parse_non_negative_quirky(context, input, allow_quirks).map(NonNegative) + } +} + +/// Either a `<length>` or the `auto` keyword. +/// +/// Note that we use LengthPercentage just for convenience, since it pretty much +/// is everything we care about, but we could just add a similar LengthOrAuto +/// instead if we think getting rid of this weirdness is worth it. +pub type LengthOrAuto = generics::LengthPercentageOrAuto<Length>; + +impl LengthOrAuto { + /// Parses a length, allowing the unitless length quirk. + /// <https://quirks.spec.whatwg.org/#the-unitless-length-quirk> + #[inline] + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with(context, input, |context, input| { + Length::parse_quirky(context, input, allow_quirks) + }) + } +} + +/// Either a non-negative `<length>` or the `auto` keyword. +pub type NonNegativeLengthOrAuto = generics::LengthPercentageOrAuto<NonNegativeLength>; + +/// Either a `<length>` or a `<number>`. +pub type LengthOrNumber = GenericLengthOrNumber<Length, Number>; + +/// A specified value for `min-width`, `min-height`, `width` or `height` property. +pub type Size = GenericSize<NonNegativeLengthPercentage>; + +impl Parse for Size { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Size::parse_quirky(context, input, AllowQuirks::No) + } +} + +impl Size { + /// Parses, with quirks. + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + #[cfg(feature = "gecko")] + { + if let Ok(l) = input.try_parse(computed::ExtremumLength::parse) { + return Ok(GenericSize::ExtremumLength(l)); + } + } + + if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() { + return Ok(GenericSize::Auto); + } + + let length = NonNegativeLengthPercentage::parse_quirky(context, input, allow_quirks)?; + Ok(GenericSize::LengthPercentage(length)) + } + + /// Returns `0%`. + #[inline] + pub fn zero_percent() -> Self { + GenericSize::LengthPercentage(NonNegativeLengthPercentage::zero_percent()) + } +} + +/// A specified value for `max-width` or `max-height` property. +pub type MaxSize = GenericMaxSize<NonNegativeLengthPercentage>; + +impl Parse for MaxSize { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + MaxSize::parse_quirky(context, input, AllowQuirks::No) + } +} + +impl MaxSize { + /// Parses, with quirks. + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + #[cfg(feature = "gecko")] + { + if let Ok(l) = input.try_parse(computed::ExtremumLength::parse) { + return Ok(GenericMaxSize::ExtremumLength(l)); + } + } + + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(GenericMaxSize::None); + } + + let length = NonNegativeLengthPercentage::parse_quirky(context, input, allow_quirks)?; + Ok(GenericMaxSize::LengthPercentage(length)) + } +} + +/// A specified non-negative `<length>` | `<number>`. +pub type NonNegativeLengthOrNumber = GenericLengthOrNumber<NonNegativeLength, NonNegativeNumber>; diff --git a/servo/components/style/values/specified/list.rs b/servo/components/style/values/specified/list.rs new file mode 100644 index 0000000000..5d31c0b34d --- /dev/null +++ b/servo/components/style/values/specified/list.rs @@ -0,0 +1,216 @@ +/* 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/. */ + +//! `list` specified values. + +use crate::parser::{Parse, ParserContext}; +#[cfg(feature = "gecko")] +use crate::values::generics::CounterStyle; +#[cfg(feature = "gecko")] +use crate::values::CustomIdent; +use cssparser::{Parser, Token}; +use style_traits::{ParseError, StyleParseErrorKind}; + +/// Specified and computed `list-style-type` property. +#[cfg(feature = "gecko")] +#[derive( + Clone, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub enum ListStyleType { + /// `none` + None, + /// <counter-style> + CounterStyle(CounterStyle), + /// <string> + String(String), +} + +#[cfg(feature = "gecko")] +impl ListStyleType { + /// Initial specified value for `list-style-type`. + #[inline] + pub fn disc() -> Self { + ListStyleType::CounterStyle(CounterStyle::disc()) + } + + /// Convert from gecko keyword to list-style-type. + /// + /// This should only be used for mapping type attribute to + /// list-style-type, and thus only values possible in that + /// attribute is considered here. + pub fn from_gecko_keyword(value: u32) -> Self { + use crate::gecko_bindings::structs; + + if value == structs::NS_STYLE_LIST_STYLE_NONE { + return ListStyleType::None; + } + + ListStyleType::CounterStyle(CounterStyle::Name(CustomIdent(match value { + structs::NS_STYLE_LIST_STYLE_DISC => atom!("disc"), + structs::NS_STYLE_LIST_STYLE_CIRCLE => atom!("circle"), + structs::NS_STYLE_LIST_STYLE_SQUARE => atom!("square"), + structs::NS_STYLE_LIST_STYLE_DECIMAL => atom!("decimal"), + structs::NS_STYLE_LIST_STYLE_LOWER_ROMAN => atom!("lower-roman"), + structs::NS_STYLE_LIST_STYLE_UPPER_ROMAN => atom!("upper-roman"), + structs::NS_STYLE_LIST_STYLE_LOWER_ALPHA => atom!("lower-alpha"), + structs::NS_STYLE_LIST_STYLE_UPPER_ALPHA => atom!("upper-alpha"), + _ => unreachable!("Unknown counter style keyword value"), + }))) + } +} + +#[cfg(feature = "gecko")] +impl Parse for ListStyleType { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(style) = input.try_parse(|i| CounterStyle::parse(context, i)) { + return Ok(ListStyleType::CounterStyle(style)); + } + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(ListStyleType::None); + } + Ok(ListStyleType::String( + input.expect_string()?.as_ref().to_owned(), + )) + } +} + +/// A quote pair. +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct QuotePair { + /// The opening quote. + pub opening: crate::OwnedStr, + + /// The closing quote. + pub closing: crate::OwnedStr, +} + +/// List of quote pairs for the specified/computed value of `quotes` property. +#[derive( + Clone, + Debug, + Default, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct QuoteList( + #[css(iterable, if_empty = "none")] + #[ignore_malloc_size_of = "Arc"] + pub crate::ArcSlice<QuotePair>, +); + +/// Specified and computed `quotes` property: `auto`, `none`, or a list +/// of characters. +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub enum Quotes { + /// list of quote pairs + QuoteList(QuoteList), + /// auto (use lang-dependent quote marks) + Auto, +} + +impl Parse for Quotes { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Quotes, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("auto")) + .is_ok() + { + return Ok(Quotes::Auto); + } + + if input + .try_parse(|input| input.expect_ident_matching("none")) + .is_ok() + { + return Ok(Quotes::QuoteList(QuoteList::default())); + } + + let mut quotes = Vec::new(); + loop { + let location = input.current_source_location(); + let opening = match input.next() { + Ok(&Token::QuotedString(ref value)) => value.as_ref().to_owned().into(), + Ok(t) => return Err(location.new_unexpected_token_error(t.clone())), + Err(_) => break, + }; + + let closing = input.expect_string()?.as_ref().to_owned().into(); + quotes.push(QuotePair { opening, closing }); + } + + if !quotes.is_empty() { + Ok(Quotes::QuoteList(QuoteList(crate::ArcSlice::from_iter( + quotes.into_iter(), + )))) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +/// Specified and computed `-moz-list-reversed` property (for UA sheets only). +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum MozListReversed { + /// the initial value + False, + /// exclusively used for <ol reversed> in our html.css UA sheet + True, +} diff --git a/servo/components/style/values/specified/mod.rs b/servo/components/style/values/specified/mod.rs new file mode 100644 index 0000000000..d6d763686e --- /dev/null +++ b/servo/components/style/values/specified/mod.rs @@ -0,0 +1,933 @@ +/* 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/. */ + +//! Specified values. +//! +//! TODO(emilio): Enhance docs. + +use super::computed::transform::DirectionVector; +use super::computed::{Context, ToComputedValue}; +use super::generics::grid::ImplicitGridTracks as GenericImplicitGridTracks; +use super::generics::grid::{GridLine as GenericGridLine, TrackBreadth as GenericTrackBreadth}; +use super::generics::grid::{TrackList as GenericTrackList, TrackSize as GenericTrackSize}; +use super::generics::transform::IsParallelTo; +use super::generics::{self, GreaterThanOrEqualToOne, NonNegative}; +use super::{CSSFloat, CSSInteger, Either, None_}; +use crate::context::QuirksMode; +use crate::parser::{Parse, ParserContext}; +use crate::values::serialize_atom_identifier; +use crate::values::specified::calc::CalcNode; +use crate::{Atom, Namespace, One, Prefix, Zero}; +use cssparser::{Parser, Token}; +use std::f32; +use std::fmt::{self, Write}; +use std::ops::Add; +use style_traits::values::specified::AllowedNumericType; +use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss}; + +#[cfg(feature = "gecko")] +pub use self::align::{AlignContent, AlignItems, AlignSelf, AlignTracks, ContentDistribution}; +#[cfg(feature = "gecko")] +pub use self::align::{JustifyContent, JustifyItems, JustifySelf, JustifyTracks, SelfAlignment}; +pub use self::angle::{AllowUnitlessZeroAngle, Angle}; +pub use self::background::{BackgroundRepeat, BackgroundSize}; +pub use self::basic_shape::FillRule; +pub use self::border::{BorderCornerRadius, BorderImageSlice, BorderImageWidth}; +pub use self::border::{BorderImageRepeat, BorderImageSideWidth}; +pub use self::border::{BorderRadius, BorderSideWidth, BorderSpacing, BorderStyle}; +pub use self::box_::{AnimationIterationCount, AnimationName, Contain, Display}; +pub use self::box_::{Appearance, BreakBetween, BreakWithin}; +pub use self::box_::{Clear, Float, Overflow, OverflowAnchor}; +pub use self::box_::{OverflowClipBox, OverscrollBehavior, Perspective, Resize}; +pub use self::box_::{ScrollSnapAlign, ScrollSnapAxis, ScrollSnapStrictness, ScrollSnapType}; +pub use self::box_::{TouchAction, TransitionProperty, VerticalAlign, WillChange}; +pub use self::color::{Color, ColorOrAuto, ColorPropertyValue}; +pub use self::column::ColumnCount; +pub use self::counters::{Content, ContentItem, CounterIncrement, CounterSetOrReset}; +pub use self::easing::TimingFunction; +pub use self::effects::{BoxShadow, Filter, SimpleShadow}; +pub use self::flex::FlexBasis; +pub use self::font::{FontFamily, FontLanguageOverride, FontStyle}; +pub use self::font::{FontFeatureSettings, FontVariantLigatures, FontVariantNumeric}; +pub use self::font::{FontSize, FontSizeAdjust, FontSizeKeyword, FontStretch, FontSynthesis}; +pub use self::font::{FontVariantAlternates, FontWeight}; +pub use self::font::{FontVariantEastAsian, FontVariationSettings}; +pub use self::font::{MathDepth, MozScriptMinSize, MozScriptSizeMultiplier, XLang, XTextZoom}; +pub use self::image::{EndingShape as GradientEndingShape, Gradient}; +pub use self::image::{Image, MozImageRect}; +pub use self::length::{AbsoluteLength, CalcLengthPercentage, CharacterWidth}; +pub use self::length::{FontRelativeLength, Length, LengthOrNumber, NonNegativeLengthOrNumber}; +pub use self::length::{LengthOrAuto, LengthPercentage, LengthPercentageOrAuto}; +pub use self::length::{MaxSize, Size}; +pub use self::length::{NoCalcLength, ViewportPercentageLength}; +pub use self::length::{ + NonNegativeLength, NonNegativeLengthPercentage, NonNegativeLengthPercentageOrAuto, +}; +#[cfg(feature = "gecko")] +pub use self::list::ListStyleType; +pub use self::list::MozListReversed; +pub use self::list::Quotes; +pub use self::motion::{OffsetPath, OffsetRotate}; +pub use self::outline::OutlineStyle; +pub use self::percentage::Percentage; +pub use self::position::AspectRatio; +pub use self::position::{ + GridAutoFlow, GridTemplateAreas, MasonryAutoFlow, Position, PositionOrAuto, +}; +pub use self::position::{PositionComponent, ZIndex}; +pub use self::rect::NonNegativeLengthOrNumberRect; +pub use self::resolution::Resolution; +pub use self::svg::MozContextProperties; +pub use self::svg::{SVGLength, SVGOpacity, SVGPaint}; +pub use self::svg::{SVGPaintOrder, SVGStrokeDashArray, SVGWidth}; +pub use self::svg_path::SVGPathData; +pub use self::text::TextAlignLast; +pub use self::text::TextUnderlinePosition; +pub use self::text::{InitialLetter, LetterSpacing, LineBreak, LineHeight, TextAlign}; +pub use self::text::{OverflowWrap, TextEmphasisPosition, TextEmphasisStyle, WordBreak}; +pub use self::text::{TextAlignKeyword, TextDecorationLine, TextOverflow, WordSpacing}; +pub use self::text::{TextDecorationLength, TextDecorationSkipInk, TextTransform}; +pub use self::time::Time; +pub use self::transform::{Rotate, Scale, Transform}; +pub use self::transform::{TransformOrigin, TransformStyle, Translate}; +#[cfg(feature = "gecko")] +pub use self::ui::CursorImage; +pub use self::ui::{Cursor, MozForceBrokenImageIcon, UserSelect}; +pub use super::generics::grid::GridTemplateComponent as GenericGridTemplateComponent; + +#[cfg(feature = "gecko")] +pub mod align; +pub mod angle; +pub mod background; +pub mod basic_shape; +pub mod border; +#[path = "box.rs"] +pub mod box_; +pub mod calc; +pub mod color; +pub mod column; +pub mod counters; +pub mod easing; +pub mod effects; +pub mod flex; +pub mod font; +#[cfg(feature = "gecko")] +pub mod gecko; +pub mod grid; +pub mod image; +pub mod length; +pub mod list; +pub mod motion; +pub mod outline; +pub mod percentage; +pub mod position; +pub mod rect; +pub mod resolution; +pub mod source_size_list; +pub mod svg; +pub mod svg_path; +pub mod text; +pub mod time; +pub mod transform; +pub mod ui; +pub mod url; + +/// <angle> | <percentage> +/// https://drafts.csswg.org/css-values/#typedef-angle-percentage +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum AngleOrPercentage { + Percentage(Percentage), + Angle(Angle), +} + +impl AngleOrPercentage { + fn parse_internal<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_unitless_zero: AllowUnitlessZeroAngle, + ) -> Result<Self, ParseError<'i>> { + if let Ok(per) = input.try_parse(|i| Percentage::parse(context, i)) { + return Ok(AngleOrPercentage::Percentage(per)); + } + + Angle::parse_internal(context, input, allow_unitless_zero).map(AngleOrPercentage::Angle) + } + + /// Allow unitless angles, used for conic-gradients as specified by the spec. + /// https://drafts.csswg.org/css-images-4/#valdef-conic-gradient-angle + pub fn parse_with_unitless<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + AngleOrPercentage::parse_internal(context, input, AllowUnitlessZeroAngle::Yes) + } +} + +impl Parse for AngleOrPercentage { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + AngleOrPercentage::parse_internal(context, input, AllowUnitlessZeroAngle::No) + } +} + +/// Parse a `<number>` value, with a given clamping mode. +fn parse_number_with_clamping_mode<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + clamping_mode: AllowedNumericType, +) -> Result<Number, ParseError<'i>> { + let location = input.current_source_location(); + match *input.next()? { + Token::Number { value, .. } if clamping_mode.is_ok(context.parsing_mode, value) => { + Ok(Number { + value: value.min(f32::MAX).max(f32::MIN), + calc_clamping_mode: None, + }) + }, + Token::Function(ref name) => { + let function = CalcNode::math_function(name, location)?; + let result = CalcNode::parse_number(context, input, function)?; + Ok(Number { + value: result.min(f32::MAX).max(f32::MIN), + calc_clamping_mode: Some(clamping_mode), + }) + }, + ref t => Err(location.new_unexpected_token_error(t.clone())), + } +} + +/// A CSS `<number>` specified value. +/// +/// https://drafts.csswg.org/css-values-3/#number-value +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, ToShmem)] +pub struct Number { + /// The numeric value itself. + value: CSSFloat, + /// If this number came from a calc() expression, this tells how clamping + /// should be done on the value. + calc_clamping_mode: Option<AllowedNumericType>, +} + +impl Parse for Number { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + parse_number_with_clamping_mode(context, input, AllowedNumericType::All) + } +} + +impl Number { + /// Returns a new number with the value `val`. + fn new_with_clamping_mode( + value: CSSFloat, + calc_clamping_mode: Option<AllowedNumericType>, + ) -> Self { + Self { + value, + calc_clamping_mode, + } + } + + /// Returns this percentage as a number. + pub fn to_percentage(&self) -> Percentage { + Percentage::new_with_clamping_mode(self.value, self.calc_clamping_mode) + } + + /// Returns a new number with the value `val`. + pub fn new(val: CSSFloat) -> Self { + Self::new_with_clamping_mode(val, None) + } + + /// Returns whether this number came from a `calc()` expression. + #[inline] + pub fn was_calc(&self) -> bool { + self.calc_clamping_mode.is_some() + } + + /// Returns the numeric value, clamped if needed. + #[inline] + pub fn get(&self) -> f32 { + self.calc_clamping_mode + .map_or(self.value, |mode| mode.clamp(self.value)) + } + + #[allow(missing_docs)] + pub fn parse_non_negative<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Number, ParseError<'i>> { + parse_number_with_clamping_mode(context, input, AllowedNumericType::NonNegative) + } + + #[allow(missing_docs)] + pub fn parse_at_least_one<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Number, ParseError<'i>> { + parse_number_with_clamping_mode(context, input, AllowedNumericType::AtLeastOne) + } + + /// Clamp to 1.0 if the value is over 1.0. + #[inline] + pub fn clamp_to_one(self) -> Self { + Number { + value: self.value.min(1.), + calc_clamping_mode: self.calc_clamping_mode, + } + } +} + +impl ToComputedValue for Number { + type ComputedValue = CSSFloat; + + #[inline] + fn to_computed_value(&self, _: &Context) -> CSSFloat { + self.get() + } + + #[inline] + fn from_computed_value(computed: &CSSFloat) -> Self { + Number { + value: *computed, + calc_clamping_mode: None, + } + } +} + +impl ToCss for Number { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.calc_clamping_mode.is_some() { + dest.write_str("calc(")?; + } + self.value.to_css(dest)?; + if self.calc_clamping_mode.is_some() { + dest.write_str(")")?; + } + Ok(()) + } +} + +impl IsParallelTo for (Number, Number, Number) { + fn is_parallel_to(&self, vector: &DirectionVector) -> bool { + use euclid::approxeq::ApproxEq; + // If a and b is parallel, the angle between them is 0deg, so + // a x b = |a|*|b|*sin(0)*n = 0 * n, |a x b| == 0. + let self_vector = DirectionVector::new(self.0.get(), self.1.get(), self.2.get()); + self_vector + .cross(*vector) + .square_length() + .approx_eq(&0.0f32) + } +} + +impl SpecifiedValueInfo for Number {} + +impl Add for Number { + type Output = Self; + + fn add(self, other: Self) -> Self { + Self::new(self.get() + other.get()) + } +} + +impl Zero for Number { + #[inline] + fn zero() -> Self { + Self::new(0.) + } + + #[inline] + fn is_zero(&self) -> bool { + self.get() == 0. + } +} + +impl From<Number> for f32 { + #[inline] + fn from(n: Number) -> Self { + n.get() + } +} + +impl From<Number> for f64 { + #[inline] + fn from(n: Number) -> Self { + n.get() as f64 + } +} + +/// A Number which is >= 0.0. +pub type NonNegativeNumber = NonNegative<Number>; + +impl Parse for NonNegativeNumber { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + parse_number_with_clamping_mode(context, input, AllowedNumericType::NonNegative) + .map(NonNegative::<Number>) + } +} + +impl One for NonNegativeNumber { + #[inline] + fn one() -> Self { + NonNegativeNumber::new(1.0) + } + + #[inline] + fn is_one(&self) -> bool { + self.get() == 1.0 + } +} + +impl NonNegativeNumber { + /// Returns a new non-negative number with the value `val`. + pub fn new(val: CSSFloat) -> Self { + NonNegative::<Number>(Number::new(val.max(0.))) + } + + /// Returns the numeric value. + #[inline] + pub fn get(&self) -> f32 { + self.0.get() + } +} + +/// A Number which is >= 1.0. +pub type GreaterThanOrEqualToOneNumber = GreaterThanOrEqualToOne<Number>; + +impl Parse for GreaterThanOrEqualToOneNumber { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + parse_number_with_clamping_mode(context, input, AllowedNumericType::AtLeastOne) + .map(GreaterThanOrEqualToOne::<Number>) + } +} + +/// <number> | <percentage> +/// +/// Accepts only non-negative numbers. +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum NumberOrPercentage { + Percentage(Percentage), + Number(Number), +} + +impl NumberOrPercentage { + fn parse_with_clamping_mode<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + type_: AllowedNumericType, + ) -> Result<Self, ParseError<'i>> { + if let Ok(per) = + input.try_parse(|i| Percentage::parse_with_clamping_mode(context, i, type_)) + { + return Ok(NumberOrPercentage::Percentage(per)); + } + + parse_number_with_clamping_mode(context, input, type_).map(NumberOrPercentage::Number) + } + + /// Parse a non-negative number or percentage. + pub fn parse_non_negative<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with_clamping_mode(context, input, AllowedNumericType::NonNegative) + } + + /// Convert the number or the percentage to a number. + pub fn to_percentage(self) -> Percentage { + match self { + Self::Percentage(p) => p, + Self::Number(n) => n.to_percentage(), + } + } + + /// Convert the number or the percentage to a number. + pub fn to_number(self) -> Number { + match self { + Self::Percentage(p) => p.to_number(), + Self::Number(n) => n, + } + } +} + +impl Parse for NumberOrPercentage { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with_clamping_mode(context, input, AllowedNumericType::All) + } +} + +/// A non-negative <number> | <percentage>. +pub type NonNegativeNumberOrPercentage = NonNegative<NumberOrPercentage>; + +impl NonNegativeNumberOrPercentage { + /// Returns the `100%` value. + #[inline] + pub fn hundred_percent() -> Self { + NonNegative(NumberOrPercentage::Percentage(Percentage::hundred())) + } +} + +impl Parse for NonNegativeNumberOrPercentage { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(NonNegative(NumberOrPercentage::parse_non_negative( + context, input, + )?)) + } +} + +/// The value of Opacity is <alpha-value>, which is "<number> | <percentage>". +/// However, we serialize the specified value as number, so it's ok to store +/// the Opacity as Number. +#[derive( + Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, SpecifiedValueInfo, ToCss, ToShmem, +)] +pub struct Opacity(Number); + +impl Parse for Opacity { + /// Opacity accepts <number> | <percentage>, so we parse it as NumberOrPercentage, + /// and then convert into an Number if it's a Percentage. + /// https://drafts.csswg.org/cssom/#serializing-css-values + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let number = NumberOrPercentage::parse(context, input)?.to_number(); + Ok(Opacity(number)) + } +} + +impl ToComputedValue for Opacity { + type ComputedValue = CSSFloat; + + #[inline] + fn to_computed_value(&self, context: &Context) -> CSSFloat { + let value = self.0.to_computed_value(context); + if context.for_smil_animation { + // SMIL expects to be able to interpolate between out-of-range + // opacity values. + value + } else { + value.min(1.0).max(0.0) + } + } + + #[inline] + fn from_computed_value(computed: &CSSFloat) -> Self { + Opacity(Number::from_computed_value(computed)) + } +} + +/// A specified `<integer>`, optionally coming from a `calc()` expression. +/// +/// <https://drafts.csswg.org/css-values/#integers> +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, PartialOrd, ToShmem)] +pub struct Integer { + value: CSSInteger, + was_calc: bool, +} + +impl Zero for Integer { + #[inline] + fn zero() -> Self { + Self::new(0) + } + + #[inline] + fn is_zero(&self) -> bool { + self.value() == 0 + } +} + +impl One for Integer { + #[inline] + fn one() -> Self { + Self::new(1) + } + + #[inline] + fn is_one(&self) -> bool { + self.value() == 1 + } +} + +impl Integer { + /// Trivially constructs a new `Integer` value. + pub fn new(val: CSSInteger) -> Self { + Integer { + value: val, + was_calc: false, + } + } + + /// Returns the integer value associated with this value. + pub fn value(&self) -> CSSInteger { + self.value + } + + /// Trivially constructs a new integer value from a `calc()` expression. + fn from_calc(val: CSSInteger) -> Self { + Integer { + value: val, + was_calc: true, + } + } +} + +impl Parse for Integer { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + match *input.next()? { + Token::Number { + int_value: Some(v), .. + } => Ok(Integer::new(v)), + Token::Function(ref name) => { + let function = CalcNode::math_function(name, location)?; + let result = CalcNode::parse_integer(context, input, function)?; + Ok(Integer::from_calc(result)) + }, + ref t => Err(location.new_unexpected_token_error(t.clone())), + } + } +} + +impl Integer { + /// Parse an integer value which is at least `min`. + pub fn parse_with_minimum<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + min: i32, + ) -> Result<Integer, ParseError<'i>> { + let value = Integer::parse(context, input)?; + // FIXME(emilio): The spec asks us to avoid rejecting it at parse + // time except until computed value time. + // + // It's not totally clear it's worth it though, and no other browser + // does this. + if value.value() < min { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(value) + } + + /// Parse a non-negative integer. + pub fn parse_non_negative<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Integer, ParseError<'i>> { + Integer::parse_with_minimum(context, input, 0) + } + + /// Parse a positive integer (>= 1). + pub fn parse_positive<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Integer, ParseError<'i>> { + Integer::parse_with_minimum(context, input, 1) + } +} + +impl ToComputedValue for Integer { + type ComputedValue = i32; + + #[inline] + fn to_computed_value(&self, _: &Context) -> i32 { + self.value + } + + #[inline] + fn from_computed_value(computed: &i32) -> Self { + Integer::new(*computed) + } +} + +impl ToCss for Integer { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.was_calc { + dest.write_str("calc(")?; + } + self.value.to_css(dest)?; + if self.was_calc { + dest.write_str(")")?; + } + Ok(()) + } +} + +impl SpecifiedValueInfo for Integer {} + +/// A wrapper of Integer, with value >= 1. +pub type PositiveInteger = GreaterThanOrEqualToOne<Integer>; + +impl Parse for PositiveInteger { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Integer::parse_positive(context, input).map(GreaterThanOrEqualToOne) + } +} + +/// A specified positive `<integer>` value or `none`. +pub type PositiveIntegerOrNone = Either<PositiveInteger, None_>; + +/// The specified value of a grid `<track-breadth>` +pub type TrackBreadth = GenericTrackBreadth<LengthPercentage>; + +/// The specified value of a grid `<track-size>` +pub type TrackSize = GenericTrackSize<LengthPercentage>; + +/// The specified value of a grid `<track-size>+` +pub type ImplicitGridTracks = GenericImplicitGridTracks<TrackSize>; + +/// The specified value of a grid `<track-list>` +/// (could also be `<auto-track-list>` or `<explicit-track-list>`) +pub type TrackList = GenericTrackList<LengthPercentage, Integer>; + +/// The specified value of a `<grid-line>`. +pub type GridLine = GenericGridLine<Integer>; + +/// `<grid-template-rows> | <grid-template-columns>` +pub type GridTemplateComponent = GenericGridTemplateComponent<LengthPercentage, Integer>; + +/// rect(...) +pub type ClipRect = generics::GenericClipRect<LengthOrAuto>; + +impl Parse for ClipRect { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_quirky(context, input, AllowQuirks::No) + } +} + +impl ClipRect { + /// Parses a rect(<top>, <left>, <bottom>, <right>), allowing quirks. + fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("rect")?; + + fn parse_argument<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<LengthOrAuto, ParseError<'i>> { + LengthOrAuto::parse_quirky(context, input, allow_quirks) + } + + input.parse_nested_block(|input| { + let top = parse_argument(context, input, allow_quirks)?; + let right; + let bottom; + let left; + + if input.try_parse(|input| input.expect_comma()).is_ok() { + right = parse_argument(context, input, allow_quirks)?; + input.expect_comma()?; + bottom = parse_argument(context, input, allow_quirks)?; + input.expect_comma()?; + left = parse_argument(context, input, allow_quirks)?; + } else { + right = parse_argument(context, input, allow_quirks)?; + bottom = parse_argument(context, input, allow_quirks)?; + left = parse_argument(context, input, allow_quirks)?; + } + + Ok(ClipRect { + top, + right, + bottom, + left, + }) + }) + } +} + +/// rect(...) | auto +pub type ClipRectOrAuto = generics::GenericClipRectOrAuto<ClipRect>; + +impl ClipRectOrAuto { + /// Parses a ClipRect or Auto, allowing quirks. + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + if let Ok(v) = input.try_parse(|i| ClipRect::parse_quirky(context, i, allow_quirks)) { + return Ok(generics::GenericClipRectOrAuto::Rect(v)); + } + input.expect_ident_matching("auto")?; + Ok(generics::GenericClipRectOrAuto::Auto) + } +} + +/// Whether quirks are allowed in this context. +#[derive(Clone, Copy, PartialEq)] +pub enum AllowQuirks { + /// Quirks are not allowed. + No, + /// Quirks are allowed, in quirks mode. + Yes, + /// Quirks are always allowed, used for SVG lengths. + Always, +} + +impl AllowQuirks { + /// Returns `true` if quirks are allowed in this context. + pub fn allowed(self, quirks_mode: QuirksMode) -> bool { + match self { + AllowQuirks::Always => true, + AllowQuirks::No => false, + AllowQuirks::Yes => quirks_mode == QuirksMode::Quirks, + } + } +} + +/// An attr(...) rule +/// +/// `[namespace? `|`]? ident` +#[derive( + Clone, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[css(function)] +#[repr(C)] +pub struct Attr { + /// Optional namespace prefix. + pub namespace_prefix: Prefix, + /// Optional namespace URL. + pub namespace_url: Namespace, + /// Attribute name + pub attribute: Atom, +} + +impl Parse for Attr { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Attr, ParseError<'i>> { + input.expect_function_matching("attr")?; + input.parse_nested_block(|i| Attr::parse_function(context, i)) + } +} + +/// Get the Namespace for a given prefix from the namespace map. +fn get_namespace_for_prefix(prefix: &Prefix, context: &ParserContext) -> Option<Namespace> { + context + .namespaces + .as_ref()? + .prefixes + .get(prefix) + .map(|x| x.clone()) +} + +impl Attr { + /// Parse contents of attr() assuming we have already parsed `attr` and are + /// within a parse_nested_block() + pub fn parse_function<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Attr, ParseError<'i>> { + // Syntax is `[namespace? `|`]? ident` + // no spaces allowed + let first = input.try_parse(|i| i.expect_ident_cloned()).ok(); + if let Ok(token) = input.try_parse(|i| i.next_including_whitespace().map(|t| t.clone())) { + match token { + Token::Delim('|') => { + let location = input.current_source_location(); + // must be followed by an ident + let second_token = match *input.next_including_whitespace()? { + Token::Ident(ref second) => second, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + }; + + let (namespace_prefix, namespace_url) = if let Some(ns) = first { + let prefix = Prefix::from(ns.as_ref()); + let ns = match get_namespace_for_prefix(&prefix, context) { + Some(ns) => ns, + None => { + return Err(location + .new_custom_error(StyleParseErrorKind::UnspecifiedError)); + }, + }; + (prefix, ns) + } else { + (Prefix::default(), Namespace::default()) + }; + return Ok(Attr { + namespace_prefix, + namespace_url, + attribute: Atom::from(second_token.as_ref()), + }); + }, + // In the case of attr(foobar ) we don't want to error out + // because of the trailing whitespace. + Token::WhiteSpace(..) => {}, + ref t => return Err(input.new_unexpected_token_error(t.clone())), + } + } + + if let Some(first) = first { + Ok(Attr { + namespace_prefix: Prefix::default(), + namespace_url: Namespace::default(), + attribute: Atom::from(first.as_ref()), + }) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +impl ToCss for Attr { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str("attr(")?; + if !self.namespace_prefix.is_empty() { + serialize_atom_identifier(&self.namespace_prefix, dest)?; + dest.write_str("|")?; + } + serialize_atom_identifier(&self.attribute, dest)?; + dest.write_str(")") + } +} diff --git a/servo/components/style/values/specified/motion.rs b/servo/components/style/values/specified/motion.rs new file mode 100644 index 0000000000..7831b78afe --- /dev/null +++ b/servo/components/style/values/specified/motion.rs @@ -0,0 +1,220 @@ +/* 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/. */ + +//! Specified types for CSS values that are related to motion path. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::motion::OffsetRotate as ComputedOffsetRotate; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::generics::motion::{GenericOffsetPath, RayFunction, RaySize}; +use crate::values::specified::{Angle, SVGPathData}; +use crate::Zero; +use cssparser::Parser; +use style_traits::{ParseError, StyleParseErrorKind}; + +/// The specified value of `offset-path`. +pub type OffsetPath = GenericOffsetPath<Angle>; + +#[cfg(feature = "gecko")] +fn is_ray_enabled() -> bool { + static_prefs::pref!("layout.css.motion-path-ray.enabled") +} +#[cfg(feature = "servo")] +fn is_ray_enabled() -> bool { + false +} + +impl Parse for RayFunction<Angle> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if !is_ray_enabled() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + let mut angle = None; + let mut size = None; + let mut contain = false; + loop { + if angle.is_none() { + angle = input.try_parse(|i| Angle::parse(context, i)).ok(); + } + + if size.is_none() { + size = input.try_parse(RaySize::parse).ok(); + if size.is_some() { + continue; + } + } + + if !contain { + contain = input + .try_parse(|i| i.expect_ident_matching("contain")) + .is_ok(); + if contain { + continue; + } + } + break; + } + + if angle.is_none() || size.is_none() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(RayFunction { + angle: angle.unwrap(), + size: size.unwrap(), + contain, + }) + } +} + +impl Parse for OffsetPath { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // Parse none. + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(OffsetPath::none()); + } + + // Parse possible functions. + let location = input.current_source_location(); + let function = input.expect_function()?.clone(); + input.parse_nested_block(move |i| { + match_ignore_ascii_case! { &function, + // Bug 1186329: Implement the parser for <basic-shape>, <geometry-box>, + // and <url>. + "path" => SVGPathData::parse(context, i).map(GenericOffsetPath::Path), + "ray" => RayFunction::parse(context, i).map(GenericOffsetPath::Ray), + _ => { + Err(location.new_custom_error( + StyleParseErrorKind::UnexpectedFunction(function.clone()) + )) + }, + } + }) + } +} + +/// The direction of offset-rotate. +#[derive(Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +#[repr(u8)] +pub enum OffsetRotateDirection { + /// Unspecified direction keyword. + #[css(skip)] + None, + /// 0deg offset (face forward). + Auto, + /// 180deg offset (face backward). + Reverse, +} + +impl OffsetRotateDirection { + /// Returns true if it is none (i.e. the keyword is not specified). + #[inline] + fn is_none(&self) -> bool { + *self == OffsetRotateDirection::None + } +} + +#[inline] +fn direction_specified_and_angle_is_zero(direction: &OffsetRotateDirection, angle: &Angle) -> bool { + !direction.is_none() && angle.is_zero() +} + +/// The specified offset-rotate. +/// The syntax is: "[ auto | reverse ] || <angle>" +/// +/// https://drafts.fxtf.org/motion-1/#offset-rotate-property +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub struct OffsetRotate { + /// [auto | reverse]. + #[css(skip_if = "OffsetRotateDirection::is_none")] + direction: OffsetRotateDirection, + /// <angle>. + /// If direction is None, this is a fixed angle which indicates a + /// constant clockwise rotation transformation applied to it by this + /// specified rotation angle. Otherwise, the angle will be added to + /// the angle of the direction in layout. + #[css(contextual_skip_if = "direction_specified_and_angle_is_zero")] + angle: Angle, +} + +impl OffsetRotate { + /// Returns the initial value, auto. + #[inline] + pub fn auto() -> Self { + OffsetRotate { + direction: OffsetRotateDirection::Auto, + angle: Angle::zero(), + } + } + + /// Returns true if self is auto 0deg. + #[inline] + pub fn is_auto(&self) -> bool { + self.direction == OffsetRotateDirection::Auto && self.angle.is_zero() + } +} + +impl Parse for OffsetRotate { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let mut direction = input.try_parse(OffsetRotateDirection::parse); + let angle = input.try_parse(|i| Angle::parse(context, i)); + if direction.is_err() { + // The direction and angle could be any order, so give it a change to parse + // direction again. + direction = input.try_parse(OffsetRotateDirection::parse); + } + + if direction.is_err() && angle.is_err() { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(OffsetRotate { + direction: direction.unwrap_or(OffsetRotateDirection::None), + angle: angle.unwrap_or(Zero::zero()), + }) + } +} + +impl ToComputedValue for OffsetRotate { + type ComputedValue = ComputedOffsetRotate; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + use crate::values::computed::Angle as ComputedAngle; + + ComputedOffsetRotate { + auto: !self.direction.is_none(), + angle: if self.direction == OffsetRotateDirection::Reverse { + // The computed value should always convert "reverse" into "auto". + // e.g. "reverse calc(20deg + 10deg)" => "auto 210deg" + self.angle.to_computed_value(context) + ComputedAngle::from_degrees(180.0) + } else { + self.angle.to_computed_value(context) + }, + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + OffsetRotate { + direction: if computed.auto { + OffsetRotateDirection::Auto + } else { + OffsetRotateDirection::None + }, + angle: ToComputedValue::from_computed_value(&computed.angle), + } + } +} diff --git a/servo/components/style/values/specified/outline.rs b/servo/components/style/values/specified/outline.rs new file mode 100644 index 0000000000..6e5382d4c2 --- /dev/null +++ b/servo/components/style/values/specified/outline.rs @@ -0,0 +1,71 @@ +/* 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/. */ + +//! Specified values for outline properties + +use crate::parser::{Parse, ParserContext}; +use crate::values::specified::BorderStyle; +use cssparser::Parser; +use selectors::parser::SelectorParseErrorKind; +use style_traits::ParseError; + +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Ord, + PartialEq, + PartialOrd, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +/// <https://drafts.csswg.org/css-ui/#propdef-outline-style> +pub enum OutlineStyle { + /// auto + Auto, + /// <border-style> + BorderStyle(BorderStyle), +} + +impl OutlineStyle { + #[inline] + /// Get default value as None + pub fn none() -> OutlineStyle { + OutlineStyle::BorderStyle(BorderStyle::None) + } + + #[inline] + /// Get value for None or Hidden + pub fn none_or_hidden(&self) -> bool { + match *self { + OutlineStyle::Auto => false, + OutlineStyle::BorderStyle(ref style) => style.none_or_hidden(), + } + } +} + +impl Parse for OutlineStyle { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<OutlineStyle, ParseError<'i>> { + if let Ok(border_style) = input.try_parse(BorderStyle::parse) { + if let BorderStyle::Hidden = border_style { + return Err(input + .new_custom_error(SelectorParseErrorKind::UnexpectedIdent("hidden".into()))); + } + + return Ok(OutlineStyle::BorderStyle(border_style)); + } + + input.expect_ident_matching("auto")?; + Ok(OutlineStyle::Auto) + } +} diff --git a/servo/components/style/values/specified/percentage.rs b/servo/components/style/values/specified/percentage.rs new file mode 100644 index 0000000000..eac4d91cf8 --- /dev/null +++ b/servo/components/style/values/specified/percentage.rs @@ -0,0 +1,174 @@ +/* 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/. */ + +//! Specified percentages. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::percentage::Percentage as ComputedPercentage; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::specified::calc::CalcNode; +use crate::values::specified::Number; +use crate::values::{serialize_percentage, CSSFloat}; +use cssparser::{Parser, Token}; +use std::fmt::{self, Write}; +use style_traits::values::specified::AllowedNumericType; +use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, ToCss}; + +/// A percentage value. +#[derive(Clone, Copy, Debug, Default, MallocSizeOf, PartialEq, ToShmem)] +pub struct Percentage { + /// The percentage value as a float. + /// + /// [0 .. 100%] maps to [0.0 .. 1.0] + value: CSSFloat, + /// If this percentage came from a calc() expression, this tells how + /// clamping should be done on the value. + calc_clamping_mode: Option<AllowedNumericType>, +} + +impl ToCss for Percentage { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.calc_clamping_mode.is_some() { + dest.write_str("calc(")?; + } + + serialize_percentage(self.value, dest)?; + + if self.calc_clamping_mode.is_some() { + dest.write_str(")")?; + } + Ok(()) + } +} + +impl Percentage { + /// Creates a percentage from a numeric value. + pub(super) fn new_with_clamping_mode( + value: CSSFloat, + calc_clamping_mode: Option<AllowedNumericType>, + ) -> Self { + Self { + value, + calc_clamping_mode, + } + } + + /// Creates a percentage from a numeric value. + pub fn new(value: CSSFloat) -> Self { + Self::new_with_clamping_mode(value, None) + } + + /// `0%` + #[inline] + pub fn zero() -> Self { + Percentage { + value: 0., + calc_clamping_mode: None, + } + } + + /// `100%` + #[inline] + pub fn hundred() -> Self { + Percentage { + value: 1., + calc_clamping_mode: None, + } + } + + /// Gets the underlying value for this float. + pub fn get(&self) -> CSSFloat { + self.calc_clamping_mode + .map_or(self.value, |mode| mode.clamp(self.value)) + } + + /// Returns this percentage as a number. + pub fn to_number(&self) -> Number { + Number::new_with_clamping_mode(self.value, self.calc_clamping_mode) + } + + /// Returns whether this percentage is a `calc()` value. + pub fn is_calc(&self) -> bool { + self.calc_clamping_mode.is_some() + } + + /// Reverses this percentage, preserving calc-ness. + /// + /// For example: If it was 20%, convert it into 80%. + pub fn reverse(&mut self) { + let new_value = 1. - self.value; + self.value = new_value; + } + + /// Parses a specific kind of percentage. + pub fn parse_with_clamping_mode<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + num_context: AllowedNumericType, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + match *input.next()? { + Token::Percentage { unit_value, .. } + if num_context.is_ok(context.parsing_mode, unit_value) => + { + Ok(Percentage::new(unit_value)) + }, + Token::Function(ref name) => { + let function = CalcNode::math_function(name, location)?; + let value = CalcNode::parse_percentage(context, input, function)?; + Ok(Percentage { + value, + calc_clamping_mode: Some(num_context), + }) + }, + ref t => Err(location.new_unexpected_token_error(t.clone())), + } + } + + /// Parses a percentage token, but rejects it if it's negative. + pub fn parse_non_negative<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with_clamping_mode(context, input, AllowedNumericType::NonNegative) + } + + /// Clamp to 100% if the value is over 100%. + #[inline] + pub fn clamp_to_hundred(self) -> Self { + Percentage { + value: self.value.min(1.), + calc_clamping_mode: self.calc_clamping_mode, + } + } +} + +impl Parse for Percentage { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with_clamping_mode(context, input, AllowedNumericType::All) + } +} + +impl ToComputedValue for Percentage { + type ComputedValue = ComputedPercentage; + + #[inline] + fn to_computed_value(&self, _: &Context) -> Self::ComputedValue { + ComputedPercentage(self.get()) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Percentage::new(computed.0) + } +} + +impl SpecifiedValueInfo for Percentage {} diff --git a/servo/components/style/values/specified/position.rs b/servo/components/style/values/specified/position.rs new file mode 100644 index 0000000000..9810959f3a --- /dev/null +++ b/servo/components/style/values/specified/position.rs @@ -0,0 +1,956 @@ +/* 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/. */ + +//! CSS handling for the specified value of +//! [`position`][position]s +//! +//! [position]: https://drafts.csswg.org/css-backgrounds-3/#position + +use crate::parser::{Parse, ParserContext}; +use crate::selector_map::PrecomputedHashMap; +use crate::str::HTML_SPACE_CHARACTERS; +use crate::values::computed::LengthPercentage as ComputedLengthPercentage; +use crate::values::computed::{Context, Percentage, ToComputedValue}; +use crate::values::generics::position::AspectRatio as GenericAspectRatio; +use crate::values::generics::position::Position as GenericPosition; +use crate::values::generics::position::PositionComponent as GenericPositionComponent; +use crate::values::generics::position::PositionOrAuto as GenericPositionOrAuto; +use crate::values::generics::position::Ratio as GenericRatio; +use crate::values::generics::position::ZIndex as GenericZIndex; +use crate::values::specified::{AllowQuirks, Integer, LengthPercentage, NonNegativeNumber}; +use crate::{Atom, One, Zero}; +use cssparser::Parser; +use selectors::parser::SelectorParseErrorKind; +use servo_arc::Arc; +use std::fmt::{self, Write}; +use style_traits::values::specified::AllowedNumericType; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +/// The specified value of a CSS `<position>` +pub type Position = GenericPosition<HorizontalPosition, VerticalPosition>; + +/// The specified value of an `auto | <position>`. +pub type PositionOrAuto = GenericPositionOrAuto<Position>; + +/// The specified value of a horizontal position. +pub type HorizontalPosition = PositionComponent<HorizontalPositionKeyword>; + +/// The specified value of a vertical position. +pub type VerticalPosition = PositionComponent<VerticalPositionKeyword>; + +/// The specified value of a component of a CSS `<position>`. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum PositionComponent<S> { + /// `center` + Center, + /// `<length-percentage>` + Length(LengthPercentage), + /// `<side> <length-percentage>?` + Side(S, Option<LengthPercentage>), +} + +/// A keyword for the X direction. +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +#[repr(u8)] +pub enum HorizontalPositionKeyword { + Left, + Right, +} + +/// A keyword for the Y direction. +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +#[repr(u8)] +pub enum VerticalPositionKeyword { + Top, + Bottom, +} + +impl Parse for Position { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let position = Self::parse_three_value_quirky(context, input, AllowQuirks::No)?; + if position.is_three_value_syntax() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(position) + } +} + +impl Position { + /// Parses a `<bg-position>`, with quirks. + pub fn parse_three_value_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + match input.try_parse(|i| PositionComponent::parse_quirky(context, i, allow_quirks)) { + Ok(x_pos @ PositionComponent::Center) => { + if let Ok(y_pos) = + input.try_parse(|i| PositionComponent::parse_quirky(context, i, allow_quirks)) + { + return Ok(Self::new(x_pos, y_pos)); + } + let x_pos = input + .try_parse(|i| PositionComponent::parse_quirky(context, i, allow_quirks)) + .unwrap_or(x_pos); + let y_pos = PositionComponent::Center; + return Ok(Self::new(x_pos, y_pos)); + }, + Ok(PositionComponent::Side(x_keyword, lp)) => { + if input + .try_parse(|i| i.expect_ident_matching("center")) + .is_ok() + { + let x_pos = PositionComponent::Side(x_keyword, lp); + let y_pos = PositionComponent::Center; + return Ok(Self::new(x_pos, y_pos)); + } + if let Ok(y_keyword) = input.try_parse(VerticalPositionKeyword::parse) { + let y_lp = input + .try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks)) + .ok(); + let x_pos = PositionComponent::Side(x_keyword, lp); + let y_pos = PositionComponent::Side(y_keyword, y_lp); + return Ok(Self::new(x_pos, y_pos)); + } + let x_pos = PositionComponent::Side(x_keyword, None); + let y_pos = lp.map_or(PositionComponent::Center, PositionComponent::Length); + return Ok(Self::new(x_pos, y_pos)); + }, + Ok(x_pos @ PositionComponent::Length(_)) => { + if let Ok(y_keyword) = input.try_parse(VerticalPositionKeyword::parse) { + let y_pos = PositionComponent::Side(y_keyword, None); + return Ok(Self::new(x_pos, y_pos)); + } + if let Ok(y_lp) = + input.try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks)) + { + let y_pos = PositionComponent::Length(y_lp); + return Ok(Self::new(x_pos, y_pos)); + } + let y_pos = PositionComponent::Center; + let _ = input.try_parse(|i| i.expect_ident_matching("center")); + return Ok(Self::new(x_pos, y_pos)); + }, + Err(_) => {}, + } + let y_keyword = VerticalPositionKeyword::parse(input)?; + let lp_and_x_pos: Result<_, ParseError> = input.try_parse(|i| { + let y_lp = i + .try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks)) + .ok(); + if let Ok(x_keyword) = i.try_parse(HorizontalPositionKeyword::parse) { + let x_lp = i + .try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks)) + .ok(); + let x_pos = PositionComponent::Side(x_keyword, x_lp); + return Ok((y_lp, x_pos)); + }; + i.expect_ident_matching("center")?; + let x_pos = PositionComponent::Center; + Ok((y_lp, x_pos)) + }); + if let Ok((y_lp, x_pos)) = lp_and_x_pos { + let y_pos = PositionComponent::Side(y_keyword, y_lp); + return Ok(Self::new(x_pos, y_pos)); + } + let x_pos = PositionComponent::Center; + let y_pos = PositionComponent::Side(y_keyword, None); + Ok(Self::new(x_pos, y_pos)) + } + + /// `center center` + #[inline] + pub fn center() -> Self { + Self::new(PositionComponent::Center, PositionComponent::Center) + } + + /// Returns true if this uses a 3 value syntax. + #[inline] + fn is_three_value_syntax(&self) -> bool { + self.horizontal.component_count() != self.vertical.component_count() + } +} + +impl ToCss for Position { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match (&self.horizontal, &self.vertical) { + ( + x_pos @ &PositionComponent::Side(_, Some(_)), + &PositionComponent::Length(ref y_lp), + ) => { + x_pos.to_css(dest)?; + dest.write_str(" top ")?; + y_lp.to_css(dest) + }, + ( + &PositionComponent::Length(ref x_lp), + y_pos @ &PositionComponent::Side(_, Some(_)), + ) => { + dest.write_str("left ")?; + x_lp.to_css(dest)?; + dest.write_str(" ")?; + y_pos.to_css(dest) + }, + (x_pos, y_pos) => { + x_pos.to_css(dest)?; + dest.write_str(" ")?; + y_pos.to_css(dest) + }, + } + } +} + +impl<S: Parse> Parse for PositionComponent<S> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_quirky(context, input, AllowQuirks::No) + } +} + +impl<S: Parse> PositionComponent<S> { + /// Parses a component of a CSS position, with quirks. + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + if input + .try_parse(|i| i.expect_ident_matching("center")) + .is_ok() + { + return Ok(PositionComponent::Center); + } + if let Ok(lp) = + input.try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks)) + { + return Ok(PositionComponent::Length(lp)); + } + let keyword = S::parse(context, input)?; + let lp = input + .try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks)) + .ok(); + Ok(PositionComponent::Side(keyword, lp)) + } +} + +impl<S> GenericPositionComponent for PositionComponent<S> { + fn is_center(&self) -> bool { + match *self { + PositionComponent::Center => true, + PositionComponent::Length(LengthPercentage::Percentage(ref per)) => per.0 == 0.5, + // 50% from any side is still the center. + PositionComponent::Side(_, Some(LengthPercentage::Percentage(ref per))) => per.0 == 0.5, + _ => false, + } + } +} + +impl<S> PositionComponent<S> { + /// `0%` + pub fn zero() -> Self { + PositionComponent::Length(LengthPercentage::Percentage(Percentage::zero())) + } + + /// Returns the count of this component. + fn component_count(&self) -> usize { + match *self { + PositionComponent::Length(..) | PositionComponent::Center => 1, + PositionComponent::Side(_, ref lp) => { + if lp.is_some() { + 2 + } else { + 1 + } + }, + } + } +} + +impl<S: Side> ToComputedValue for PositionComponent<S> { + type ComputedValue = ComputedLengthPercentage; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + PositionComponent::Center => ComputedLengthPercentage::new_percent(Percentage(0.5)), + PositionComponent::Side(ref keyword, None) => { + let p = Percentage(if keyword.is_start() { 0. } else { 1. }); + ComputedLengthPercentage::new_percent(p) + }, + PositionComponent::Side(ref keyword, Some(ref length)) if !keyword.is_start() => { + let length = length.to_computed_value(context); + // We represent `<end-side> <length>` as `calc(100% - <length>)`. + ComputedLengthPercentage::hundred_percent_minus(length, AllowedNumericType::All) + }, + PositionComponent::Side(_, Some(ref length)) | + PositionComponent::Length(ref length) => length.to_computed_value(context), + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + PositionComponent::Length(ToComputedValue::from_computed_value(computed)) + } +} + +impl<S: Side> PositionComponent<S> { + /// The initial specified value of a position component, i.e. the start side. + pub fn initial_specified_value() -> Self { + PositionComponent::Side(S::start(), None) + } +} + +/// Represents a side, either horizontal or vertical, of a CSS position. +pub trait Side { + /// Returns the start side. + fn start() -> Self; + + /// Returns whether this side is the start side. + fn is_start(&self) -> bool; +} + +impl Side for HorizontalPositionKeyword { + #[inline] + fn start() -> Self { + HorizontalPositionKeyword::Left + } + + #[inline] + fn is_start(&self) -> bool { + *self == Self::start() + } +} + +impl Side for VerticalPositionKeyword { + #[inline] + fn start() -> Self { + VerticalPositionKeyword::Top + } + + #[inline] + fn is_start(&self) -> bool { + *self == Self::start() + } +} + +bitflags! { + /// Controls how the auto-placement algorithm works + /// specifying exactly how auto-placed items get flowed into the grid + #[derive( + MallocSizeOf, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem + )] + #[value_info(other_values = "row,column,dense")] + #[repr(C)] + pub struct GridAutoFlow: u8 { + /// 'row' - mutually exclusive with 'column' + const ROW = 1 << 0; + /// 'column' - mutually exclusive with 'row' + const COLUMN = 1 << 1; + /// 'dense' + const DENSE = 1 << 2; + } +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +/// Masonry auto-placement algorithm packing. +pub enum MasonryPlacement { + /// Place the item in the track(s) with the smallest extent so far. + Pack, + /// Place the item after the last item, from start to end. + Next, +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +/// Masonry auto-placement algorithm item sorting option. +pub enum MasonryItemOrder { + /// Place all items with a definite placement before auto-placed items. + DefiniteFirst, + /// Place items in `order-modified document order`. + Ordered, +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +/// Controls how the Masonry layout algorithm works +/// specifying exactly how auto-placed items get flowed in the masonry axis. +pub struct MasonryAutoFlow { + /// Specify how to pick a auto-placement track. + #[css(contextual_skip_if = "is_pack_with_non_default_order")] + pub placement: MasonryPlacement, + /// Specify how to pick an item to place. + #[css(skip_if = "is_item_order_definite_first")] + pub order: MasonryItemOrder, +} + +#[inline] +fn is_pack_with_non_default_order(placement: &MasonryPlacement, order: &MasonryItemOrder) -> bool { + *placement == MasonryPlacement::Pack && *order != MasonryItemOrder::DefiniteFirst +} + +#[inline] +fn is_item_order_definite_first(order: &MasonryItemOrder) -> bool { + *order == MasonryItemOrder::DefiniteFirst +} + +impl MasonryAutoFlow { + #[inline] + /// Get initial `masonry-auto-flow` value. + pub fn initial() -> MasonryAutoFlow { + MasonryAutoFlow { + placement: MasonryPlacement::Pack, + order: MasonryItemOrder::DefiniteFirst, + } + } +} + +impl Parse for MasonryAutoFlow { + /// [ definite-first | ordered ] || [ pack | next ] + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<MasonryAutoFlow, ParseError<'i>> { + let mut value = MasonryAutoFlow::initial(); + let mut got_placement = false; + let mut got_order = false; + while !input.is_exhausted() { + let location = input.current_source_location(); + let ident = input.expect_ident()?; + let success = match_ignore_ascii_case! { &ident, + "pack" if !got_placement => { + got_placement = true; + true + }, + "next" if !got_placement => { + value.placement = MasonryPlacement::Next; + got_placement = true; + true + }, + "definite-first" if !got_order => { + got_order = true; + true + }, + "ordered" if !got_order => { + value.order = MasonryItemOrder::Ordered; + got_order = true; + true + }, + _ => false + }; + if !success { + return Err(location + .new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone()))); + } + } + + if got_placement || got_order { + Ok(value) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +#[cfg(feature = "gecko")] +impl From<u8> for MasonryAutoFlow { + fn from(bits: u8) -> MasonryAutoFlow { + use crate::gecko_bindings::structs; + let mut value = MasonryAutoFlow::initial(); + if bits & structs::NS_STYLE_MASONRY_PLACEMENT_PACK as u8 == 0 { + value.placement = MasonryPlacement::Next; + } + if bits & structs::NS_STYLE_MASONRY_ORDER_DEFINITE_FIRST as u8 == 0 { + value.order = MasonryItemOrder::Ordered; + } + value + } +} + +#[cfg(feature = "gecko")] +impl From<MasonryAutoFlow> for u8 { + fn from(v: MasonryAutoFlow) -> u8 { + use crate::gecko_bindings::structs; + + let mut result: u8 = 0; + if v.placement == MasonryPlacement::Pack { + result |= structs::NS_STYLE_MASONRY_PLACEMENT_PACK as u8; + } + if v.order == MasonryItemOrder::DefiniteFirst { + result |= structs::NS_STYLE_MASONRY_ORDER_DEFINITE_FIRST as u8; + } + result + } +} + +impl Parse for GridAutoFlow { + /// [ row | column ] || dense + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<GridAutoFlow, ParseError<'i>> { + let mut track = None; + let mut dense = GridAutoFlow::empty(); + + while !input.is_exhausted() { + let location = input.current_source_location(); + let ident = input.expect_ident()?; + let success = match_ignore_ascii_case! { &ident, + "row" if track.is_none() => { + track = Some(GridAutoFlow::ROW); + true + }, + "column" if track.is_none() => { + track = Some(GridAutoFlow::COLUMN); + true + }, + "dense" if dense.is_empty() => { + dense = GridAutoFlow::DENSE; + true + }, + _ => false, + }; + if !success { + return Err(location + .new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone()))); + } + } + + if track.is_some() || !dense.is_empty() { + Ok(track.unwrap_or(GridAutoFlow::ROW) | dense) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +impl ToCss for GridAutoFlow { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if *self == GridAutoFlow::ROW { + return dest.write_str("row"); + } + + if *self == GridAutoFlow::COLUMN { + return dest.write_str("column"); + } + + if *self == GridAutoFlow::ROW | GridAutoFlow::DENSE { + return dest.write_str("dense"); + } + + if *self == GridAutoFlow::COLUMN | GridAutoFlow::DENSE { + return dest.write_str("column dense"); + } + + debug_assert!(false, "Unknown or invalid grid-autoflow value"); + Ok(()) + } +} + +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +/// https://drafts.csswg.org/css-grid/#named-grid-area +pub struct TemplateAreas { + /// `named area` containing for each template area + #[css(skip)] + pub areas: crate::OwnedSlice<NamedArea>, + /// The original CSS string value of each template area + #[css(iterable)] + pub strings: crate::OwnedSlice<crate::OwnedStr>, + /// The number of columns of the grid. + #[css(skip)] + pub width: u32, +} + +impl TemplateAreas { + /// Transform `vector` of str into `template area` + pub fn from_vec(strings: Vec<crate::OwnedStr>) -> Result<Self, ()> { + if strings.is_empty() { + return Err(()); + } + let mut areas: Vec<NamedArea> = vec![]; + let mut width = 0; + { + let mut row = 0u32; + let mut area_indices = PrecomputedHashMap::<Atom, usize>::default(); + for string in &strings { + let mut current_area_index: Option<usize> = None; + row += 1; + let mut column = 0u32; + for token in TemplateAreasTokenizer(string) { + column += 1; + let name = if let Some(token) = token? { + Atom::from(token) + } else { + if let Some(index) = current_area_index.take() { + if areas[index].columns.end != column { + return Err(()); + } + } + continue; + }; + if let Some(index) = current_area_index { + if areas[index].name == name { + if areas[index].rows.start == row { + areas[index].columns.end += 1; + } + continue; + } + if areas[index].columns.end != column { + return Err(()); + } + } + if let Some(index) = area_indices.get(&name).cloned() { + if areas[index].columns.start != column || areas[index].rows.end != row { + return Err(()); + } + areas[index].rows.end += 1; + current_area_index = Some(index); + continue; + } + let index = areas.len(); + assert!(area_indices.insert(name.clone(), index).is_none()); + areas.push(NamedArea { + name, + columns: UnsignedRange { + start: column, + end: column + 1, + }, + rows: UnsignedRange { + start: row, + end: row + 1, + }, + }); + current_area_index = Some(index); + } + if column == 0 { + // Each string must produce a valid token. + // https://github.com/w3c/csswg-drafts/issues/5110 + return Err(()); + } + if let Some(index) = current_area_index { + if areas[index].columns.end != column + 1 { + assert_ne!(areas[index].rows.start, row); + return Err(()); + } + } + if row == 1 { + width = column; + } else if width != column { + return Err(()); + } + } + } + Ok(TemplateAreas { + areas: areas.into(), + strings: strings.into(), + width, + }) + } +} + +impl Parse for TemplateAreas { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut strings = vec![]; + while let Ok(string) = + input.try_parse(|i| i.expect_string().map(|s| s.as_ref().to_owned().into())) + { + strings.push(string); + } + + TemplateAreas::from_vec(strings) + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } +} + +/// Arc type for `Arc<TemplateAreas>` +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct TemplateAreasArc(#[ignore_malloc_size_of = "Arc"] pub Arc<TemplateAreas>); + +impl Parse for TemplateAreasArc { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let parsed = TemplateAreas::parse(context, input)?; + Ok(TemplateAreasArc(Arc::new(parsed))) + } +} + +/// A range of rows or columns. Using this instead of std::ops::Range for FFI +/// purposes. +#[repr(C)] +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +pub struct UnsignedRange { + /// The start of the range. + pub start: u32, + /// The end of the range. + pub end: u32, +} + +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +/// Not associated with any particular grid item, but can be referenced from the +/// grid-placement properties. +pub struct NamedArea { + /// Name of the `named area` + pub name: Atom, + /// Rows of the `named area` + pub rows: UnsignedRange, + /// Columns of the `named area` + pub columns: UnsignedRange, +} + +/// Tokenize the string into a list of the tokens, +/// using longest-match semantics +struct TemplateAreasTokenizer<'a>(&'a str); + +impl<'a> Iterator for TemplateAreasTokenizer<'a> { + type Item = Result<Option<&'a str>, ()>; + + fn next(&mut self) -> Option<Self::Item> { + let rest = self.0.trim_start_matches(HTML_SPACE_CHARACTERS); + if rest.is_empty() { + return None; + } + if rest.starts_with('.') { + self.0 = &rest[rest.find(|c| c != '.').unwrap_or(rest.len())..]; + return Some(Ok(None)); + } + if !rest.starts_with(is_name_code_point) { + return Some(Err(())); + } + let token_len = rest.find(|c| !is_name_code_point(c)).unwrap_or(rest.len()); + let token = &rest[..token_len]; + self.0 = &rest[token_len..]; + Some(Ok(Some(token))) + } +} + +fn is_name_code_point(c: char) -> bool { + c >= 'A' && c <= 'Z' || + c >= 'a' && c <= 'z' || + c >= '\u{80}' || + c == '_' || + c >= '0' && c <= '9' || + c == '-' +} + +/// This property specifies named grid areas. +/// +/// The syntax of this property also provides a visualization of the structure +/// of the grid, making the overall layout of the grid container easier to +/// understand. +#[repr(C, u8)] +#[derive( + Clone, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub enum GridTemplateAreas { + /// The `none` value. + None, + /// The actual value. + Areas(TemplateAreasArc), +} + +impl GridTemplateAreas { + #[inline] + /// Get default value as `none` + pub fn none() -> GridTemplateAreas { + GridTemplateAreas::None + } +} + +/// A specified value for the `z-index` property. +pub type ZIndex = GenericZIndex<Integer>; + +/// A specified value for the `aspect-ratio` property. +pub type AspectRatio = GenericAspectRatio<NonNegativeNumber>; + +impl Parse for AspectRatio { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + use crate::values::generics::position::PreferredRatio; + + let location = input.current_source_location(); + let mut auto = input.try_parse(|i| i.expect_ident_matching("auto")); + let ratio = input.try_parse(|i| Ratio::parse(context, i)); + if auto.is_err() { + auto = input.try_parse(|i| i.expect_ident_matching("auto")); + } + + if auto.is_err() && ratio.is_err() { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(AspectRatio { + auto: auto.is_ok(), + ratio: match ratio { + Ok(ratio) => PreferredRatio::Ratio(ratio), + Err(..) => PreferredRatio::None, + }, + }) + } +} + +impl AspectRatio { + /// Returns Self by a valid ratio. + pub fn from_mapped_ratio(w: f32, h: f32) -> Self { + use crate::values::generics::position::PreferredRatio; + AspectRatio { + auto: true, + ratio: PreferredRatio::Ratio(GenericRatio( + NonNegativeNumber::new(w), + NonNegativeNumber::new(h), + )), + } + } +} + +/// A specified <ratio> value. +pub type Ratio = GenericRatio<NonNegativeNumber>; + +// https://drafts.csswg.org/css-values-4/#ratios +impl Parse for Ratio { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let a = NonNegativeNumber::parse(context, input)?; + let b = match input.try_parse(|input| input.expect_delim('/')) { + Ok(()) => NonNegativeNumber::parse(context, input)?, + _ => One::one(), + }; + + Ok(GenericRatio(a, b)) + } +} diff --git a/servo/components/style/values/specified/rect.rs b/servo/components/style/values/specified/rect.rs new file mode 100644 index 0000000000..7955ecaa48 --- /dev/null +++ b/servo/components/style/values/specified/rect.rs @@ -0,0 +1,11 @@ +/* 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/. */ + +//! Specified types for CSS borders. + +use crate::values::generics::rect::Rect; +use crate::values::specified::length::NonNegativeLengthOrNumber; + +/// A specified rectangle made of four `<length-or-number>` values. +pub type NonNegativeLengthOrNumberRect = Rect<NonNegativeLengthOrNumber>; diff --git a/servo/components/style/values/specified/resolution.rs b/servo/components/style/values/specified/resolution.rs new file mode 100644 index 0000000000..e8674cffaa --- /dev/null +++ b/servo/components/style/values/specified/resolution.rs @@ -0,0 +1,77 @@ +/* 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/. */ + +//! Resolution values: +//! +//! https://drafts.csswg.org/css-values/#resolution + +use crate::parser::{Parse, ParserContext}; +use crate::values::CSSFloat; +use cssparser::{Parser, Token}; +use style_traits::{ParseError, StyleParseErrorKind}; + +/// A specified resolution. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem, SpecifiedValueInfo)] +pub enum Resolution { + /// Dots per inch. + #[css(dimension)] + Dpi(CSSFloat), + /// An alias unit for dots per pixel. + #[css(dimension)] + X(CSSFloat), + /// Dots per pixel. + #[css(dimension)] + Dppx(CSSFloat), + /// Dots per centimeter. + #[css(dimension)] + Dpcm(CSSFloat), +} + +impl Resolution { + /// Convert this resolution value to dppx units. + pub fn to_dppx(&self) -> CSSFloat { + match *self { + Resolution::X(f) | Resolution::Dppx(f) => f, + _ => self.to_dpi() / 96.0, + } + } + + /// Convert this resolution value to dpi units. + pub fn to_dpi(&self) -> CSSFloat { + match *self { + Resolution::Dpi(f) => f, + Resolution::X(f) | Resolution::Dppx(f) => f * 96.0, + Resolution::Dpcm(f) => f * 2.54, + } + } +} + +impl Parse for Resolution { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let (value, unit) = match *input.next()? { + Token::Dimension { + value, ref unit, .. + } => (value, unit), + ref t => return Err(location.new_unexpected_token_error(t.clone())), + }; + + if value <= 0. { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + match_ignore_ascii_case! { &unit, + "dpi" => Ok(Resolution::Dpi(value)), + "dppx" => Ok(Resolution::Dppx(value)), + "dpcm" => Ok(Resolution::Dpcm(value)), + "x" => Ok(Resolution::X(value)), + _ => Err(location.new_custom_error( + StyleParseErrorKind::UnexpectedDimension(unit.clone()) + )), + } + } +} diff --git a/servo/components/style/values/specified/source_size_list.rs b/servo/components/style/values/specified/source_size_list.rs new file mode 100644 index 0000000000..edb2c09f73 --- /dev/null +++ b/servo/components/style/values/specified/source_size_list.rs @@ -0,0 +1,145 @@ +/* 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/. */ + +//! https://html.spec.whatwg.org/multipage/#source-size-list + +#[cfg(feature = "gecko")] +use crate::gecko_bindings::sugar::ownership::{HasBoxFFI, HasFFI, HasSimpleFFI}; +use crate::media_queries::{Device, MediaCondition}; +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::{self, ToComputedValue}; +use crate::values::specified::{Length, NoCalcLength, ViewportPercentageLength}; +use app_units::Au; +use cssparser::{Delimiter, Parser, Token}; +use selectors::context::QuirksMode; +use style_traits::ParseError; + +/// A value for a `<source-size>`: +/// +/// https://html.spec.whatwg.org/multipage/#source-size +#[derive(Debug)] +pub struct SourceSize { + condition: MediaCondition, + value: Length, +} + +impl Parse for SourceSize { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let condition = MediaCondition::parse(context, input)?; + let value = Length::parse_non_negative(context, input)?; + + Ok(Self { condition, value }) + } +} + +/// A value for a `<source-size-list>`: +/// +/// https://html.spec.whatwg.org/multipage/#source-size-list +#[derive(Debug)] +pub struct SourceSizeList { + source_sizes: Vec<SourceSize>, + value: Option<Length>, +} + +impl SourceSizeList { + /// Create an empty `SourceSizeList`, which can be used as a fall-back. + pub fn empty() -> Self { + Self { + source_sizes: vec![], + value: None, + } + } + + /// Evaluate this <source-size-list> to get the final viewport length. + pub fn evaluate(&self, device: &Device, quirks_mode: QuirksMode) -> Au { + let matching_source_size = self + .source_sizes + .iter() + .find(|source_size| source_size.condition.matches(device, quirks_mode)); + + computed::Context::for_media_query_evaluation(device, quirks_mode, |context| { + match matching_source_size { + Some(source_size) => source_size.value.to_computed_value(context), + None => match self.value { + Some(ref v) => v.to_computed_value(context), + None => Length::NoCalc(NoCalcLength::ViewportPercentage( + ViewportPercentageLength::Vw(100.), + )) + .to_computed_value(context), + }, + } + }) + .into() + } +} + +enum SourceSizeOrLength { + SourceSize(SourceSize), + Length(Length), +} + +impl Parse for SourceSizeOrLength { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(size) = input.try_parse(|input| SourceSize::parse(context, input)) { + return Ok(SourceSizeOrLength::SourceSize(size)); + } + + let length = Length::parse_non_negative(context, input)?; + Ok(SourceSizeOrLength::Length(length)) + } +} + +impl SourceSizeList { + /// NOTE(emilio): This doesn't match the grammar in the spec, see: + /// + /// https://html.spec.whatwg.org/multipage/#parsing-a-sizes-attribute + pub fn parse<'i, 't>(context: &ParserContext, input: &mut Parser<'i, 't>) -> Self { + let mut source_sizes = vec![]; + + loop { + let result = input.parse_until_before(Delimiter::Comma, |input| { + SourceSizeOrLength::parse(context, input) + }); + + match result { + Ok(SourceSizeOrLength::Length(value)) => { + return Self { + source_sizes, + value: Some(value), + }; + }, + Ok(SourceSizeOrLength::SourceSize(source_size)) => { + source_sizes.push(source_size); + }, + Err(..) => {}, + } + + match input.next() { + Ok(&Token::Comma) => {}, + Err(..) => break, + _ => unreachable!(), + } + } + + SourceSizeList { + source_sizes, + value: None, + } + } +} + +#[cfg(feature = "gecko")] +unsafe impl HasFFI for SourceSizeList { + type FFIType = crate::gecko_bindings::structs::RawServoSourceSizeList; +} +#[cfg(feature = "gecko")] +unsafe impl HasSimpleFFI for SourceSizeList {} +#[cfg(feature = "gecko")] +unsafe impl HasBoxFFI for SourceSizeList {} diff --git a/servo/components/style/values/specified/svg.rs b/servo/components/style/values/specified/svg.rs new file mode 100644 index 0000000000..abc6587239 --- /dev/null +++ b/servo/components/style/values/specified/svg.rs @@ -0,0 +1,336 @@ +/* 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/. */ + +//! Specified types for SVG properties. + +use crate::parser::{Parse, ParserContext}; +use crate::values::generics::svg as generic; +use crate::values::specified::color::Color; +use crate::values::specified::url::SpecifiedUrl; +use crate::values::specified::AllowQuirks; +use crate::values::specified::LengthPercentage; +use crate::values::specified::{NonNegativeLengthPercentage, Opacity}; +use crate::values::CustomIdent; +use cssparser::{Parser, Token}; +use std::fmt::{self, Write}; +use style_traits::{CommaWithSpace, CssWriter, ParseError, Separator}; +use style_traits::{StyleParseErrorKind, ToCss}; + +/// Specified SVG Paint value +pub type SVGPaint = generic::GenericSVGPaint<Color, SpecifiedUrl>; + +/// <length> | <percentage> | <number> | context-value +pub type SVGLength = generic::GenericSVGLength<LengthPercentage>; + +/// A non-negative version of SVGLength. +pub type SVGWidth = generic::GenericSVGLength<NonNegativeLengthPercentage>; + +/// [ <length> | <percentage> | <number> ]# | context-value +pub type SVGStrokeDashArray = generic::GenericSVGStrokeDashArray<NonNegativeLengthPercentage>; + +/// Whether the `context-value` value is enabled. +#[cfg(feature = "gecko")] +pub fn is_context_value_enabled() -> bool { + static_prefs::pref!("gfx.font_rendering.opentype_svg.enabled") +} + +/// Whether the `context-value` value is enabled. +#[cfg(not(feature = "gecko"))] +pub fn is_context_value_enabled() -> bool { + false +} + +macro_rules! parse_svg_length { + ($ty:ty, $lp:ty) => { + impl Parse for $ty { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(lp) = + input.try_parse(|i| <$lp>::parse_quirky(context, i, AllowQuirks::Always)) + { + return Ok(generic::SVGLength::LengthPercentage(lp)); + } + + try_match_ident_ignore_ascii_case! { input, + "context-value" if is_context_value_enabled() => { + Ok(generic::SVGLength::ContextValue) + }, + } + } + } + }; +} + +parse_svg_length!(SVGLength, LengthPercentage); +parse_svg_length!(SVGWidth, NonNegativeLengthPercentage); + +impl Parse for SVGStrokeDashArray { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(values) = input.try_parse(|i| { + CommaWithSpace::parse(i, |i| { + NonNegativeLengthPercentage::parse_quirky(context, i, AllowQuirks::Always) + }) + }) { + return Ok(generic::SVGStrokeDashArray::Values(values.into())); + } + + try_match_ident_ignore_ascii_case! { input, + "context-value" if is_context_value_enabled() => { + Ok(generic::SVGStrokeDashArray::ContextValue) + }, + "none" => Ok(generic::SVGStrokeDashArray::Values(Default::default())), + } + } +} + +/// <opacity-value> | context-fill-opacity | context-stroke-opacity +pub type SVGOpacity = generic::SVGOpacity<Opacity>; + +/// The specified value for a single CSS paint-order property. +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, ToCss)] +pub enum PaintOrder { + /// `normal` variant + Normal = 0, + /// `fill` variant + Fill = 1, + /// `stroke` variant + Stroke = 2, + /// `markers` variant + Markers = 3, +} + +/// Number of non-normal components +pub const PAINT_ORDER_COUNT: u8 = 3; + +/// Number of bits for each component +pub const PAINT_ORDER_SHIFT: u8 = 2; + +/// Mask with above bits set +pub const PAINT_ORDER_MASK: u8 = 0b11; + +/// The specified value is tree `PaintOrder` values packed into the +/// bitfields below, as a six-bit field, of 3 two-bit pairs +/// +/// Each pair can be set to FILL, STROKE, or MARKERS +/// Lowest significant bit pairs are highest priority. +/// `normal` is the empty bitfield. The three pairs are +/// never zero in any case other than `normal`. +/// +/// Higher priority values, i.e. the values specified first, +/// will be painted first (and may be covered by paintings of lower priority) +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct SVGPaintOrder(pub u8); + +impl SVGPaintOrder { + /// Get default `paint-order` with `0` + pub fn normal() -> Self { + SVGPaintOrder(0) + } + + /// Get variant of `paint-order` + pub fn order_at(&self, pos: u8) -> PaintOrder { + match (self.0 >> pos * PAINT_ORDER_SHIFT) & PAINT_ORDER_MASK { + 0 => PaintOrder::Normal, + 1 => PaintOrder::Fill, + 2 => PaintOrder::Stroke, + 3 => PaintOrder::Markers, + _ => unreachable!("this cannot happen"), + } + } +} + +impl Parse for SVGPaintOrder { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<SVGPaintOrder, ParseError<'i>> { + if let Ok(()) = input.try_parse(|i| i.expect_ident_matching("normal")) { + return Ok(SVGPaintOrder::normal()); + } + + let mut value = 0; + // bitfield representing what we've seen so far + // bit 1 is fill, bit 2 is stroke, bit 3 is markers + let mut seen = 0; + let mut pos = 0; + + loop { + let result: Result<_, ParseError> = input.try_parse(|input| { + try_match_ident_ignore_ascii_case! { input, + "fill" => Ok(PaintOrder::Fill), + "stroke" => Ok(PaintOrder::Stroke), + "markers" => Ok(PaintOrder::Markers), + } + }); + + match result { + Ok(val) => { + if (seen & (1 << val as u8)) != 0 { + // don't parse the same ident twice + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + value |= (val as u8) << (pos * PAINT_ORDER_SHIFT); + seen |= 1 << (val as u8); + pos += 1; + }, + Err(_) => break, + } + } + + if value == 0 { + // Couldn't find any keyword + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + // fill in rest + for i in pos..PAINT_ORDER_COUNT { + for paint in 1..(PAINT_ORDER_COUNT + 1) { + // if not seen, set bit at position, mark as seen + if (seen & (1 << paint)) == 0 { + seen |= 1 << paint; + value |= paint << (i * PAINT_ORDER_SHIFT); + break; + } + } + } + + Ok(SVGPaintOrder(value)) + } +} + +impl ToCss for SVGPaintOrder { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.0 == 0 { + return dest.write_str("normal"); + } + + let mut last_pos_to_serialize = 0; + for i in (1..PAINT_ORDER_COUNT).rev() { + let component = self.order_at(i); + let earlier_component = self.order_at(i - 1); + if component < earlier_component { + last_pos_to_serialize = i - 1; + break; + } + } + + for pos in 0..last_pos_to_serialize + 1 { + if pos != 0 { + dest.write_str(" ")? + } + self.order_at(pos).to_css(dest)?; + } + Ok(()) + } +} + +bitflags! { + /// The context properties we understand. + #[derive(Default, MallocSizeOf, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem)] + #[repr(C)] + pub struct ContextPropertyBits: u8 { + /// `fill` + const FILL = 1 << 0; + /// `stroke` + const STROKE = 1 << 1; + /// `fill-opacity` + const FILL_OPACITY = 1 << 2; + /// `stroke-opacity` + const STROKE_OPACITY = 1 << 3; + } +} + +/// Specified MozContextProperties value. +/// Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-context-properties) +#[derive( + Clone, + Debug, + Default, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct MozContextProperties { + #[css(iterable, if_empty = "none")] + #[ignore_malloc_size_of = "Arc"] + idents: crate::ArcSlice<CustomIdent>, + #[css(skip)] + bits: ContextPropertyBits, +} + +impl Parse for MozContextProperties { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<MozContextProperties, ParseError<'i>> { + let mut values = vec![]; + let mut bits = ContextPropertyBits::empty(); + loop { + { + let location = input.current_source_location(); + let ident = input.expect_ident()?; + + if ident.eq_ignore_ascii_case("none") && values.is_empty() { + return Ok(Self::default()); + } + + let ident = CustomIdent::from_ident(location, ident, &["all", "none", "auto"])?; + + if ident.0 == atom!("fill") { + bits.insert(ContextPropertyBits::FILL); + } else if ident.0 == atom!("stroke") { + bits.insert(ContextPropertyBits::STROKE); + } else if ident.0 == atom!("fill-opacity") { + bits.insert(ContextPropertyBits::FILL_OPACITY); + } else if ident.0 == atom!("stroke-opacity") { + bits.insert(ContextPropertyBits::STROKE_OPACITY); + } + + values.push(ident); + } + + let location = input.current_source_location(); + match input.next() { + Ok(&Token::Comma) => continue, + Err(..) => break, + Ok(other) => return Err(location.new_unexpected_token_error(other.clone())), + } + } + + if values.is_empty() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(MozContextProperties { + idents: crate::ArcSlice::from_iter(values.into_iter()), + bits, + }) + } +} diff --git a/servo/components/style/values/specified/svg_path.rs b/servo/components/style/values/specified/svg_path.rs new file mode 100644 index 0000000000..9f1c5c5e32 --- /dev/null +++ b/servo/components/style/values/specified/svg_path.rs @@ -0,0 +1,896 @@ +/* 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/. */ + +//! Specified types for SVG Path. + +use crate::parser::{Parse, ParserContext}; +use crate::values::animated::{Animate, Procedure, ToAnimatedZero}; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::values::CSSFloat; +use cssparser::Parser; +use std::fmt::{self, Write}; +use std::iter::{Cloned, Peekable}; +use std::slice; +use style_traits::values::SequenceWriter; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +/// The SVG path data. +/// +/// https://www.w3.org/TR/SVG11/paths.html#PathData +#[derive( + Clone, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct SVGPathData( + // TODO(emilio): Should probably measure this somehow only from the + // specified values. + #[ignore_malloc_size_of = "Arc"] pub crate::ArcSlice<PathCommand>, +); + +impl SVGPathData { + /// Get the array of PathCommand. + #[inline] + pub fn commands(&self) -> &[PathCommand] { + &self.0 + } + + /// Create a normalized copy of this path by converting each relative + /// command to an absolute command. + pub fn normalize(&self) -> Self { + let mut state = PathTraversalState { + subpath_start: CoordPair::new(0.0, 0.0), + pos: CoordPair::new(0.0, 0.0), + }; + let result = self + .0 + .iter() + .map(|seg| seg.normalize(&mut state)) + .collect::<Vec<_>>(); + + SVGPathData(crate::ArcSlice::from_iter(result.into_iter())) + } +} + +impl ToCss for SVGPathData { + #[inline] + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + dest.write_char('"')?; + { + let mut writer = SequenceWriter::new(dest, " "); + for command in self.commands() { + writer.item(command)?; + } + } + dest.write_char('"') + } +} + +impl Parse for SVGPathData { + // We cannot use cssparser::Parser to parse a SVG path string because the spec wants to make + // the SVG path string as compact as possible. (i.e. The whitespaces may be dropped.) + // e.g. "M100 200L100 200" is a valid SVG path string. If we use tokenizer, the first ident + // is "M100", instead of "M", and this is not correct. Therefore, we use a Peekable + // str::Char iterator to check each character. + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let path_string = input.expect_string()?.as_ref(); + + // Parse the svg path string as multiple sub-paths. + let mut path_parser = PathParser::new(path_string); + while skip_wsp(&mut path_parser.chars) { + if path_parser.parse_subpath().is_err() { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + } + + Ok(SVGPathData(crate::ArcSlice::from_iter( + path_parser.path.into_iter(), + ))) + } +} + +impl Animate for SVGPathData { + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + if self.0.len() != other.0.len() { + return Err(()); + } + + // FIXME(emilio): This allocates three copies of the path, that's not + // great! Specially, once we're normalized once, we don't need to + // re-normalize again. + let result = self + .normalize() + .0 + .iter() + .zip(other.normalize().0.iter()) + .map(|(a, b)| a.animate(&b, procedure)) + .collect::<Result<Vec<_>, _>>()?; + + Ok(SVGPathData(crate::ArcSlice::from_iter(result.into_iter()))) + } +} + +impl ComputeSquaredDistance for SVGPathData { + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + if self.0.len() != other.0.len() { + return Err(()); + } + self.normalize() + .0 + .iter() + .zip(other.normalize().0.iter()) + .map(|(this, other)| this.compute_squared_distance(&other)) + .sum() + } +} + +/// The SVG path command. +/// The fields of these commands are self-explanatory, so we skip the documents. +/// Note: the index of the control points, e.g. control1, control2, are mapping to the control +/// points of the Bézier curve in the spec. +/// +/// https://www.w3.org/TR/SVG11/paths.html#PathData +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +#[repr(C, u8)] +pub enum PathCommand { + /// The unknown type. + /// https://www.w3.org/TR/SVG/paths.html#__svg__SVGPathSeg__PATHSEG_UNKNOWN + Unknown, + /// The "moveto" command. + MoveTo { + point: CoordPair, + absolute: IsAbsolute, + }, + /// The "lineto" command. + LineTo { + point: CoordPair, + absolute: IsAbsolute, + }, + /// The horizontal "lineto" command. + HorizontalLineTo { x: CSSFloat, absolute: IsAbsolute }, + /// The vertical "lineto" command. + VerticalLineTo { y: CSSFloat, absolute: IsAbsolute }, + /// The cubic Bézier curve command. + CurveTo { + control1: CoordPair, + control2: CoordPair, + point: CoordPair, + absolute: IsAbsolute, + }, + /// The smooth curve command. + SmoothCurveTo { + control2: CoordPair, + point: CoordPair, + absolute: IsAbsolute, + }, + /// The quadratic Bézier curve command. + QuadBezierCurveTo { + control1: CoordPair, + point: CoordPair, + absolute: IsAbsolute, + }, + /// The smooth quadratic Bézier curve command. + SmoothQuadBezierCurveTo { + point: CoordPair, + absolute: IsAbsolute, + }, + /// The elliptical arc curve command. + EllipticalArc { + rx: CSSFloat, + ry: CSSFloat, + angle: CSSFloat, + large_arc_flag: ArcFlag, + sweep_flag: ArcFlag, + point: CoordPair, + absolute: IsAbsolute, + }, + /// The "closepath" command. + ClosePath, +} + +/// For internal SVGPath normalization. +#[allow(missing_docs)] +struct PathTraversalState { + subpath_start: CoordPair, + pos: CoordPair, +} + +impl PathCommand { + /// Create a normalized copy of this PathCommand. Absolute commands will be copied as-is while + /// for relative commands an equivalent absolute command will be returned. + /// + /// See discussion: https://github.com/w3c/svgwg/issues/321 + fn normalize(&self, state: &mut PathTraversalState) -> Self { + use self::PathCommand::*; + match *self { + Unknown => Unknown, + ClosePath => { + state.pos = state.subpath_start; + ClosePath + }, + MoveTo { + mut point, + absolute, + } => { + if !absolute.is_yes() { + point += state.pos; + } + state.pos = point; + state.subpath_start = point; + MoveTo { + point, + absolute: IsAbsolute::Yes, + } + }, + LineTo { + mut point, + absolute, + } => { + if !absolute.is_yes() { + point += state.pos; + } + state.pos = point; + LineTo { + point, + absolute: IsAbsolute::Yes, + } + }, + HorizontalLineTo { mut x, absolute } => { + if !absolute.is_yes() { + x += state.pos.0; + } + state.pos.0 = x; + HorizontalLineTo { + x, + absolute: IsAbsolute::Yes, + } + }, + VerticalLineTo { mut y, absolute } => { + if !absolute.is_yes() { + y += state.pos.1; + } + state.pos.1 = y; + VerticalLineTo { + y, + absolute: IsAbsolute::Yes, + } + }, + CurveTo { + mut control1, + mut control2, + mut point, + absolute, + } => { + if !absolute.is_yes() { + control1 += state.pos; + control2 += state.pos; + point += state.pos; + } + state.pos = point; + CurveTo { + control1, + control2, + point, + absolute: IsAbsolute::Yes, + } + }, + SmoothCurveTo { + mut control2, + mut point, + absolute, + } => { + if !absolute.is_yes() { + control2 += state.pos; + point += state.pos; + } + state.pos = point; + SmoothCurveTo { + control2, + point, + absolute: IsAbsolute::Yes, + } + }, + QuadBezierCurveTo { + mut control1, + mut point, + absolute, + } => { + if !absolute.is_yes() { + control1 += state.pos; + point += state.pos; + } + state.pos = point; + QuadBezierCurveTo { + control1, + point, + absolute: IsAbsolute::Yes, + } + }, + SmoothQuadBezierCurveTo { + mut point, + absolute, + } => { + if !absolute.is_yes() { + point += state.pos; + } + state.pos = point; + SmoothQuadBezierCurveTo { + point, + absolute: IsAbsolute::Yes, + } + }, + EllipticalArc { + rx, + ry, + angle, + large_arc_flag, + sweep_flag, + mut point, + absolute, + } => { + if !absolute.is_yes() { + point += state.pos; + } + state.pos = point; + EllipticalArc { + rx, + ry, + angle, + large_arc_flag, + sweep_flag, + point, + absolute: IsAbsolute::Yes, + } + }, + } + } +} + +impl ToCss for PathCommand { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + use self::PathCommand::*; + match *self { + Unknown => dest.write_char('X'), + ClosePath => dest.write_char('Z'), + MoveTo { point, absolute } => { + dest.write_char(if absolute.is_yes() { 'M' } else { 'm' })?; + dest.write_char(' ')?; + point.to_css(dest) + }, + LineTo { point, absolute } => { + dest.write_char(if absolute.is_yes() { 'L' } else { 'l' })?; + dest.write_char(' ')?; + point.to_css(dest) + }, + CurveTo { + control1, + control2, + point, + absolute, + } => { + dest.write_char(if absolute.is_yes() { 'C' } else { 'c' })?; + dest.write_char(' ')?; + control1.to_css(dest)?; + dest.write_char(' ')?; + control2.to_css(dest)?; + dest.write_char(' ')?; + point.to_css(dest) + }, + QuadBezierCurveTo { + control1, + point, + absolute, + } => { + dest.write_char(if absolute.is_yes() { 'Q' } else { 'q' })?; + dest.write_char(' ')?; + control1.to_css(dest)?; + dest.write_char(' ')?; + point.to_css(dest) + }, + EllipticalArc { + rx, + ry, + angle, + large_arc_flag, + sweep_flag, + point, + absolute, + } => { + dest.write_char(if absolute.is_yes() { 'A' } else { 'a' })?; + dest.write_char(' ')?; + rx.to_css(dest)?; + dest.write_char(' ')?; + ry.to_css(dest)?; + dest.write_char(' ')?; + angle.to_css(dest)?; + dest.write_char(' ')?; + large_arc_flag.to_css(dest)?; + dest.write_char(' ')?; + sweep_flag.to_css(dest)?; + dest.write_char(' ')?; + point.to_css(dest) + }, + HorizontalLineTo { x, absolute } => { + dest.write_char(if absolute.is_yes() { 'H' } else { 'h' })?; + dest.write_char(' ')?; + x.to_css(dest) + }, + VerticalLineTo { y, absolute } => { + dest.write_char(if absolute.is_yes() { 'V' } else { 'v' })?; + dest.write_char(' ')?; + y.to_css(dest) + }, + SmoothCurveTo { + control2, + point, + absolute, + } => { + dest.write_char(if absolute.is_yes() { 'S' } else { 's' })?; + dest.write_char(' ')?; + control2.to_css(dest)?; + dest.write_char(' ')?; + point.to_css(dest) + }, + SmoothQuadBezierCurveTo { point, absolute } => { + dest.write_char(if absolute.is_yes() { 'T' } else { 't' })?; + dest.write_char(' ')?; + point.to_css(dest) + }, + } + } +} + +/// The path command absolute type. +#[allow(missing_docs)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum IsAbsolute { + Yes, + No, +} + +impl IsAbsolute { + /// Return true if this is IsAbsolute::Yes. + #[inline] + pub fn is_yes(&self) -> bool { + *self == IsAbsolute::Yes + } +} + +/// The path coord type. +#[derive( + AddAssign, + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct CoordPair(CSSFloat, CSSFloat); + +impl CoordPair { + /// Create a CoordPair. + #[inline] + pub fn new(x: CSSFloat, y: CSSFloat) -> Self { + CoordPair(x, y) + } +} + +/// The EllipticalArc flag type. +#[derive( + Clone, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct ArcFlag(bool); + +impl ToCss for ArcFlag { + #[inline] + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + (self.0 as i32).to_css(dest) + } +} + +impl Animate for ArcFlag { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + (self.0 as i32) + .animate(&(other.0 as i32), procedure) + .map(|v| ArcFlag(v > 0)) + } +} + +impl ComputeSquaredDistance for ArcFlag { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + (self.0 as i32).compute_squared_distance(&(other.0 as i32)) + } +} + +impl ToAnimatedZero for ArcFlag { + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + // The 2 ArcFlags in EllipticalArc determine which one of the 4 different arcs will be + // used. (i.e. From 4 combinations). In other words, if we change the flag, we get a + // different arc. Therefore, we return *self. + // https://svgwg.org/svg2-draft/paths.html#PathDataEllipticalArcCommands + Ok(*self) + } +} + +/// SVG Path parser. +struct PathParser<'a> { + chars: Peekable<Cloned<slice::Iter<'a, u8>>>, + path: Vec<PathCommand>, +} + +macro_rules! parse_arguments { + ( + $parser:ident, + $abs:ident, + $enum:ident, + [ $para:ident => $func:ident $(, $other_para:ident => $other_func:ident)* ] + ) => { + { + loop { + let $para = $func(&mut $parser.chars)?; + $( + skip_comma_wsp(&mut $parser.chars); + let $other_para = $other_func(&mut $parser.chars)?; + )* + $parser.path.push(PathCommand::$enum { $para $(, $other_para)*, $abs }); + + // End of string or the next character is a possible new command. + if !skip_wsp(&mut $parser.chars) || + $parser.chars.peek().map_or(true, |c| c.is_ascii_alphabetic()) { + break; + } + skip_comma_wsp(&mut $parser.chars); + } + Ok(()) + } + } +} + +impl<'a> PathParser<'a> { + /// Return a PathParser. + #[inline] + fn new(string: &'a str) -> Self { + PathParser { + chars: string.as_bytes().iter().cloned().peekable(), + path: Vec::new(), + } + } + + /// Parse a sub-path. + fn parse_subpath(&mut self) -> Result<(), ()> { + // Handle "moveto" Command first. If there is no "moveto", this is not a valid sub-path + // (i.e. not a valid moveto-drawto-command-group). + self.parse_moveto()?; + + // Handle other commands. + loop { + skip_wsp(&mut self.chars); + if self.chars.peek().map_or(true, |&m| m == b'M' || m == b'm') { + break; + } + + let command = self.chars.next().unwrap(); + let abs = if command.is_ascii_uppercase() { + IsAbsolute::Yes + } else { + IsAbsolute::No + }; + + skip_wsp(&mut self.chars); + match command { + b'Z' | b'z' => self.parse_closepath(), + b'L' | b'l' => self.parse_lineto(abs), + b'H' | b'h' => self.parse_h_lineto(abs), + b'V' | b'v' => self.parse_v_lineto(abs), + b'C' | b'c' => self.parse_curveto(abs), + b'S' | b's' => self.parse_smooth_curveto(abs), + b'Q' | b'q' => self.parse_quadratic_bezier_curveto(abs), + b'T' | b't' => self.parse_smooth_quadratic_bezier_curveto(abs), + b'A' | b'a' => self.parse_elliptical_arc(abs), + _ => return Err(()), + }?; + } + Ok(()) + } + + /// Parse "moveto" command. + fn parse_moveto(&mut self) -> Result<(), ()> { + let command = match self.chars.next() { + Some(c) if c == b'M' || c == b'm' => c, + _ => return Err(()), + }; + + skip_wsp(&mut self.chars); + let point = parse_coord(&mut self.chars)?; + let absolute = if command == b'M' { + IsAbsolute::Yes + } else { + IsAbsolute::No + }; + self.path.push(PathCommand::MoveTo { point, absolute }); + + // End of string or the next character is a possible new command. + if !skip_wsp(&mut self.chars) || self.chars.peek().map_or(true, |c| c.is_ascii_alphabetic()) + { + return Ok(()); + } + skip_comma_wsp(&mut self.chars); + + // If a moveto is followed by multiple pairs of coordinates, the subsequent + // pairs are treated as implicit lineto commands. + self.parse_lineto(absolute) + } + + /// Parse "closepath" command. + fn parse_closepath(&mut self) -> Result<(), ()> { + self.path.push(PathCommand::ClosePath); + Ok(()) + } + + /// Parse "lineto" command. + fn parse_lineto(&mut self, absolute: IsAbsolute) -> Result<(), ()> { + parse_arguments!(self, absolute, LineTo, [ point => parse_coord ]) + } + + /// Parse horizontal "lineto" command. + fn parse_h_lineto(&mut self, absolute: IsAbsolute) -> Result<(), ()> { + parse_arguments!(self, absolute, HorizontalLineTo, [ x => parse_number ]) + } + + /// Parse vertical "lineto" command. + fn parse_v_lineto(&mut self, absolute: IsAbsolute) -> Result<(), ()> { + parse_arguments!(self, absolute, VerticalLineTo, [ y => parse_number ]) + } + + /// Parse cubic Bézier curve command. + fn parse_curveto(&mut self, absolute: IsAbsolute) -> Result<(), ()> { + parse_arguments!(self, absolute, CurveTo, [ + control1 => parse_coord, control2 => parse_coord, point => parse_coord + ]) + } + + /// Parse smooth "curveto" command. + fn parse_smooth_curveto(&mut self, absolute: IsAbsolute) -> Result<(), ()> { + parse_arguments!(self, absolute, SmoothCurveTo, [ + control2 => parse_coord, point => parse_coord + ]) + } + + /// Parse quadratic Bézier curve command. + fn parse_quadratic_bezier_curveto(&mut self, absolute: IsAbsolute) -> Result<(), ()> { + parse_arguments!(self, absolute, QuadBezierCurveTo, [ + control1 => parse_coord, point => parse_coord + ]) + } + + /// Parse smooth quadratic Bézier curveto command. + fn parse_smooth_quadratic_bezier_curveto(&mut self, absolute: IsAbsolute) -> Result<(), ()> { + parse_arguments!(self, absolute, SmoothQuadBezierCurveTo, [ point => parse_coord ]) + } + + /// Parse elliptical arc curve command. + fn parse_elliptical_arc(&mut self, absolute: IsAbsolute) -> Result<(), ()> { + // Parse a flag whose value is '0' or '1'; otherwise, return Err(()). + let parse_flag = |iter: &mut Peekable<Cloned<slice::Iter<u8>>>| match iter.next() { + Some(c) if c == b'0' || c == b'1' => Ok(ArcFlag(c == b'1')), + _ => Err(()), + }; + parse_arguments!(self, absolute, EllipticalArc, [ + rx => parse_number, + ry => parse_number, + angle => parse_number, + large_arc_flag => parse_flag, + sweep_flag => parse_flag, + point => parse_coord + ]) + } +} + +/// Parse a pair of numbers into CoordPair. +fn parse_coord(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> Result<CoordPair, ()> { + let x = parse_number(iter)?; + skip_comma_wsp(iter); + let y = parse_number(iter)?; + Ok(CoordPair::new(x, y)) +} + +/// This is a special version which parses the number for SVG Path. e.g. "M 0.6.5" should be parsed +/// as MoveTo with a coordinate of ("0.6", ".5"), instead of treating 0.6.5 as a non-valid floating +/// point number. In other words, the logic here is similar with that of +/// tokenizer::consume_numeric, which also consumes the number as many as possible, but here the +/// input is a Peekable and we only accept an integer of a floating point number. +/// +/// The "number" syntax in https://www.w3.org/TR/SVG/paths.html#PathDataBNF +fn parse_number(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> Result<CSSFloat, ()> { + // 1. Check optional sign. + let sign = if iter + .peek() + .map_or(false, |&sign| sign == b'+' || sign == b'-') + { + if iter.next().unwrap() == b'-' { + -1. + } else { + 1. + } + } else { + 1. + }; + + // 2. Check integer part. + let mut integral_part: f64 = 0.; + let got_dot = if !iter.peek().map_or(false, |&n| n == b'.') { + // If the first digit in integer part is neither a dot nor a digit, this is not a number. + if iter.peek().map_or(true, |n| !n.is_ascii_digit()) { + return Err(()); + } + + while iter.peek().map_or(false, |n| n.is_ascii_digit()) { + integral_part = integral_part * 10. + (iter.next().unwrap() - b'0') as f64; + } + + iter.peek().map_or(false, |&n| n == b'.') + } else { + true + }; + + // 3. Check fractional part. + let mut fractional_part: f64 = 0.; + if got_dot { + // Consume '.'. + iter.next(); + // If the first digit in fractional part is not a digit, this is not a number. + if iter.peek().map_or(true, |n| !n.is_ascii_digit()) { + return Err(()); + } + + let mut factor = 0.1; + while iter.peek().map_or(false, |n| n.is_ascii_digit()) { + fractional_part += (iter.next().unwrap() - b'0') as f64 * factor; + factor *= 0.1; + } + } + + let mut value = sign * (integral_part + fractional_part); + + // 4. Check exp part. The segment name of SVG Path doesn't include 'E' or 'e', so it's ok to + // treat the numbers after 'E' or 'e' are in the exponential part. + if iter.peek().map_or(false, |&exp| exp == b'E' || exp == b'e') { + // Consume 'E' or 'e'. + iter.next(); + let exp_sign = if iter + .peek() + .map_or(false, |&sign| sign == b'+' || sign == b'-') + { + if iter.next().unwrap() == b'-' { + -1. + } else { + 1. + } + } else { + 1. + }; + + let mut exp: f64 = 0.; + while iter.peek().map_or(false, |n| n.is_ascii_digit()) { + exp = exp * 10. + (iter.next().unwrap() - b'0') as f64; + } + + value *= f64::powf(10., exp * exp_sign); + } + + if value.is_finite() { + Ok(value + .min(::std::f32::MAX as f64) + .max(::std::f32::MIN as f64) as CSSFloat) + } else { + Err(()) + } +} + +/// Skip all svg whitespaces, and return true if |iter| hasn't finished. +#[inline] +fn skip_wsp(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> bool { + // Note: SVG 1.1 defines the whitespaces as \u{9}, \u{20}, \u{A}, \u{D}. + // However, SVG 2 has one extra whitespace: \u{C}. + // Therefore, we follow the newest spec for the definition of whitespace, + // i.e. \u{9}, \u{20}, \u{A}, \u{C}, \u{D}. + while iter.peek().map_or(false, |c| c.is_ascii_whitespace()) { + iter.next(); + } + iter.peek().is_some() +} + +/// Skip all svg whitespaces and one comma, and return true if |iter| hasn't finished. +#[inline] +fn skip_comma_wsp(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> bool { + if !skip_wsp(iter) { + return false; + } + + if *iter.peek().unwrap() != b',' { + return true; + } + iter.next(); + + skip_wsp(iter) +} diff --git a/servo/components/style/values/specified/text.rs b/servo/components/style/values/specified/text.rs new file mode 100644 index 0000000000..7c5c712eb3 --- /dev/null +++ b/servo/components/style/values/specified/text.rs @@ -0,0 +1,1195 @@ +/* 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/. */ + +//! Specified types for text properties. + +use crate::parser::{Parse, ParserContext}; +use crate::properties::longhands::writing_mode::computed_value::T as SpecifiedWritingMode; +use crate::values::computed::text::LineHeight as ComputedLineHeight; +use crate::values::computed::text::TextEmphasisStyle as ComputedTextEmphasisStyle; +use crate::values::computed::text::TextOverflow as ComputedTextOverflow; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::generics::text::InitialLetter as GenericInitialLetter; +use crate::values::generics::text::LineHeight as GenericLineHeight; +use crate::values::generics::text::{GenericTextDecorationLength, Spacing}; +use crate::values::specified::length::NonNegativeLengthPercentage; +use crate::values::specified::length::{FontRelativeLength, Length}; +use crate::values::specified::length::{LengthPercentage, NoCalcLength}; +use crate::values::specified::{AllowQuirks, Integer, NonNegativeNumber, Number}; +use cssparser::{Parser, Token}; +use selectors::parser::SelectorParseErrorKind; +use std::fmt::{self, Write}; +use style_traits::values::SequenceWriter; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; +use unicode_segmentation::UnicodeSegmentation; + +/// A specified type for the `initial-letter` property. +pub type InitialLetter = GenericInitialLetter<Number, Integer>; + +/// A specified value for the `letter-spacing` property. +pub type LetterSpacing = Spacing<Length>; + +/// A specified value for the `word-spacing` property. +pub type WordSpacing = Spacing<LengthPercentage>; + +/// A specified value for the `line-height` property. +pub type LineHeight = GenericLineHeight<NonNegativeNumber, NonNegativeLengthPercentage>; + +impl Parse for InitialLetter { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input + .try_parse(|i| i.expect_ident_matching("normal")) + .is_ok() + { + return Ok(GenericInitialLetter::Normal); + } + let size = Number::parse_at_least_one(context, input)?; + let sink = input + .try_parse(|i| Integer::parse_positive(context, i)) + .ok(); + Ok(GenericInitialLetter::Specified(size, sink)) + } +} + +impl Parse for LetterSpacing { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Spacing::parse_with(context, input, |c, i| { + Length::parse_quirky(c, i, AllowQuirks::Yes) + }) + } +} + +impl Parse for WordSpacing { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Spacing::parse_with(context, input, |c, i| { + LengthPercentage::parse_quirky(c, i, AllowQuirks::Yes) + }) + } +} + +impl ToComputedValue for LineHeight { + type ComputedValue = ComputedLineHeight; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + use crate::values::specified::length::FontBaseSize; + match *self { + GenericLineHeight::Normal => GenericLineHeight::Normal, + #[cfg(feature = "gecko")] + GenericLineHeight::MozBlockHeight => GenericLineHeight::MozBlockHeight, + GenericLineHeight::Number(number) => { + GenericLineHeight::Number(number.to_computed_value(context)) + }, + GenericLineHeight::Length(ref non_negative_lp) => { + let result = match non_negative_lp.0 { + LengthPercentage::Length(NoCalcLength::Absolute(ref abs)) => { + context.maybe_zoom_text(abs.to_computed_value(context)) + }, + LengthPercentage::Length(ref length) => length.to_computed_value(context), + LengthPercentage::Percentage(ref p) => FontRelativeLength::Em(p.0) + .to_computed_value(context, FontBaseSize::CurrentStyle), + LengthPercentage::Calc(ref calc) => { + let computed_calc = + calc.to_computed_value_zoomed(context, FontBaseSize::CurrentStyle); + let base = context.style().get_font().clone_font_size().size(); + computed_calc.resolve(base) + }, + }; + GenericLineHeight::Length(result.into()) + }, + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + match *computed { + GenericLineHeight::Normal => GenericLineHeight::Normal, + #[cfg(feature = "gecko")] + GenericLineHeight::MozBlockHeight => GenericLineHeight::MozBlockHeight, + GenericLineHeight::Number(ref number) => { + GenericLineHeight::Number(NonNegativeNumber::from_computed_value(number)) + }, + GenericLineHeight::Length(ref length) => { + GenericLineHeight::Length(NoCalcLength::from_computed_value(&length.0).into()) + }, + } + } +} + +/// A generic value for the `text-overflow` property. +#[derive( + Clone, + Debug, + Eq, + MallocSizeOf, + PartialEq, + Parse, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum TextOverflowSide { + /// Clip inline content. + Clip, + /// Render ellipsis to represent clipped inline content. + Ellipsis, + /// Render a given string to represent clipped inline content. + String(crate::OwnedStr), +} + +#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +/// text-overflow. Specifies rendering when inline content overflows its line box edge. +pub struct TextOverflow { + /// First value. Applies to end line box edge if no second is supplied; line-left edge otherwise. + pub first: TextOverflowSide, + /// Second value. Applies to the line-right edge if supplied. + pub second: Option<TextOverflowSide>, +} + +impl Parse for TextOverflow { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<TextOverflow, ParseError<'i>> { + let first = TextOverflowSide::parse(context, input)?; + let second = input + .try_parse(|input| TextOverflowSide::parse(context, input)) + .ok(); + Ok(TextOverflow { first, second }) + } +} + +impl ToComputedValue for TextOverflow { + type ComputedValue = ComputedTextOverflow; + + #[inline] + fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue { + if let Some(ref second) = self.second { + Self::ComputedValue { + first: self.first.clone(), + second: second.clone(), + sides_are_logical: false, + } + } else { + Self::ComputedValue { + first: TextOverflowSide::Clip, + second: self.first.clone(), + sides_are_logical: true, + } + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + if computed.sides_are_logical { + assert_eq!(computed.first, TextOverflowSide::Clip); + TextOverflow { + first: computed.second.clone(), + second: None, + } + } else { + TextOverflow { + first: computed.first.clone(), + second: Some(computed.second.clone()), + } + } + } +} + +bitflags! { + #[derive(MallocSizeOf, Serialize, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem)] + #[value_info(other_values = "none,underline,overline,line-through,blink")] + #[repr(C)] + /// Specified keyword values for the text-decoration-line property. + pub struct TextDecorationLine: u8 { + /// No text decoration line is specified. + const NONE = 0; + /// underline + const UNDERLINE = 1 << 0; + /// overline + const OVERLINE = 1 << 1; + /// line-through + const LINE_THROUGH = 1 << 2; + /// blink + const BLINK = 1 << 3; + /// Only set by presentation attributes + /// + /// Setting this will mean that text-decorations use the color + /// specified by `color` in quirks mode. + /// + /// For example, this gives <a href=foo><font color="red">text</font></a> + /// a red text decoration + #[cfg(feature = "gecko")] + const COLOR_OVERRIDE = 0x10; + } +} + +impl Default for TextDecorationLine { + fn default() -> Self { + TextDecorationLine::NONE + } +} + +impl Parse for TextDecorationLine { + /// none | [ underline || overline || line-through || blink ] + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut result = TextDecorationLine::empty(); + + // NOTE(emilio): this loop has this weird structure because we run this + // code to parse the text-decoration shorthand as well, so we need to + // ensure we don't return an error if we don't consume the whole thing + // because we find an invalid identifier or other kind of token. + loop { + let flag: Result<_, ParseError<'i>> = input.try_parse(|input| { + let flag = try_match_ident_ignore_ascii_case! { input, + "none" if result.is_empty() => TextDecorationLine::NONE, + "underline" => TextDecorationLine::UNDERLINE, + "overline" => TextDecorationLine::OVERLINE, + "line-through" => TextDecorationLine::LINE_THROUGH, + "blink" => TextDecorationLine::BLINK, + }; + + Ok(flag) + }); + + let flag = match flag { + Ok(flag) => flag, + Err(..) => break, + }; + + if flag.is_empty() { + return Ok(TextDecorationLine::NONE); + } + + if result.contains(flag) { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + result.insert(flag) + } + + if !result.is_empty() { + Ok(result) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +impl ToCss for TextDecorationLine { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.is_empty() { + return dest.write_str("none"); + } + + #[cfg(feature = "gecko")] + { + if *self == TextDecorationLine::COLOR_OVERRIDE { + return Ok(()); + } + } + + let mut writer = SequenceWriter::new(dest, " "); + let mut any = false; + + macro_rules! maybe_write { + ($ident:ident => $str:expr) => { + if self.contains(TextDecorationLine::$ident) { + any = true; + writer.raw_item($str)?; + } + }; + } + + maybe_write!(UNDERLINE => "underline"); + maybe_write!(OVERLINE => "overline"); + maybe_write!(LINE_THROUGH => "line-through"); + maybe_write!(BLINK => "blink"); + + debug_assert!(any); + + Ok(()) + } +} + +impl TextDecorationLine { + #[inline] + /// Returns the initial value of text-decoration-line + pub fn none() -> Self { + TextDecorationLine::NONE + } +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +/// Specified value of the text-transform property, stored in two parts: +/// the case-related transforms (mutually exclusive, only one may be in effect), and others (non-exclusive). +pub struct TextTransform { + /// Case transform, if any. + pub case_: TextTransformCase, + /// Non-case transforms. + pub other_: TextTransformOther, +} + +impl TextTransform { + #[inline] + /// Returns the initial value of text-transform + pub fn none() -> Self { + TextTransform { + case_: TextTransformCase::None, + other_: TextTransformOther::empty(), + } + } + #[inline] + /// Returns whether the value is 'none' + pub fn is_none(&self) -> bool { + self.case_ == TextTransformCase::None && self.other_.is_empty() + } +} + +impl Parse for TextTransform { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut result = TextTransform::none(); + + // Case keywords are mutually exclusive; other transforms may co-occur. + loop { + let location = input.current_source_location(); + let ident = match input.next() { + Ok(&Token::Ident(ref ident)) => ident, + Ok(other) => return Err(location.new_unexpected_token_error(other.clone())), + Err(..) => break, + }; + + match_ignore_ascii_case! { ident, + "none" if result.is_none() => { + return Ok(result); + }, + "uppercase" if result.case_ == TextTransformCase::None => { + result.case_ = TextTransformCase::Uppercase + }, + "lowercase" if result.case_ == TextTransformCase::None => { + result.case_ = TextTransformCase::Lowercase + }, + "capitalize" if result.case_ == TextTransformCase::None => { + result.case_ = TextTransformCase::Capitalize + }, + "full-width" if !result.other_.intersects(TextTransformOther::FULL_WIDTH) => { + result.other_.insert(TextTransformOther::FULL_WIDTH) + }, + "full-size-kana" if !result.other_.intersects(TextTransformOther::FULL_SIZE_KANA) => { + result.other_.insert(TextTransformOther::FULL_SIZE_KANA) + }, + _ => return Err(location.new_custom_error( + SelectorParseErrorKind::UnexpectedIdent(ident.clone()) + )), + } + } + + if result.is_none() { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } else { + Ok(result) + } + } +} + +impl ToCss for TextTransform { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.is_none() { + return dest.write_str("none"); + } + + if self.case_ != TextTransformCase::None { + self.case_.to_css(dest)?; + if !self.other_.is_empty() { + dest.write_str(" ")?; + } + } + + self.other_.to_css(dest) + } +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +/// Specified keyword values for case transforms in the text-transform property. (These are exclusive.) +pub enum TextTransformCase { + /// No case transform. + None, + /// All uppercase. + Uppercase, + /// All lowercase. + Lowercase, + /// Capitalize each word. + Capitalize, +} + +bitflags! { + #[derive(MallocSizeOf, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem)] + #[value_info(other_values = "none,full-width,full-size-kana")] + #[repr(C)] + /// Specified keyword values for non-case transforms in the text-transform property. (Non-exclusive.) + pub struct TextTransformOther: u8 { + /// full-width + const FULL_WIDTH = 1 << 0; + /// full-size-kana + const FULL_SIZE_KANA = 1 << 1; + } +} + +impl ToCss for TextTransformOther { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + let mut writer = SequenceWriter::new(dest, " "); + let mut any = false; + macro_rules! maybe_write { + ($ident:ident => $str:expr) => { + if self.contains(TextTransformOther::$ident) { + writer.raw_item($str)?; + any = true; + } + }; + } + + maybe_write!(FULL_WIDTH => "full-width"); + maybe_write!(FULL_SIZE_KANA => "full-size-kana"); + + debug_assert!(any || self.is_empty()); + + Ok(()) + } +} + +/// Specified and computed value of text-align-last. +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +#[repr(u8)] +pub enum TextAlignLast { + Auto, + Start, + End, + Left, + Right, + Center, + Justify, +} + +/// Specified value of text-align keyword value. +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +#[repr(u8)] +pub enum TextAlignKeyword { + Start, + Left, + Right, + Center, + #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))] + Justify, + #[css(skip)] + #[cfg(feature = "gecko")] + Char, + End, + #[cfg(feature = "gecko")] + MozCenter, + #[cfg(feature = "gecko")] + MozLeft, + #[cfg(feature = "gecko")] + MozRight, + #[cfg(feature = "servo-layout-2013")] + ServoCenter, + #[cfg(feature = "servo-layout-2013")] + ServoLeft, + #[cfg(feature = "servo-layout-2013")] + ServoRight, +} + +/// Specified value of text-align property. +#[derive( + Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +pub enum TextAlign { + /// Keyword value of text-align property. + Keyword(TextAlignKeyword), + /// `match-parent` value of text-align property. It has a different handling + /// unlike other keywords. + #[cfg(feature = "gecko")] + MatchParent, + /// `MozCenterOrInherit` value of text-align property. It cannot be parsed, + /// only set directly on the elements and it has a different handling + /// unlike other values. + #[cfg(feature = "gecko")] + #[css(skip)] + MozCenterOrInherit, +} + +impl ToComputedValue for TextAlign { + type ComputedValue = TextAlignKeyword; + + #[inline] + fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue { + match *self { + TextAlign::Keyword(key) => key, + #[cfg(feature = "gecko")] + TextAlign::MatchParent => { + // on the root <html> element we should still respect the dir + // but the parent dir of that element is LTR even if it's <html dir=rtl> + // and will only be RTL if certain prefs have been set. + // In that case, the default behavior here will set it to left, + // but we want to set it to right -- instead set it to the default (`start`), + // which will do the right thing in this case (but not the general case) + if _context.builder.is_root_element { + return TextAlignKeyword::Start; + } + let parent = _context + .builder + .get_parent_inherited_text() + .clone_text_align(); + let ltr = _context.builder.inherited_writing_mode().is_bidi_ltr(); + match (parent, ltr) { + (TextAlignKeyword::Start, true) => TextAlignKeyword::Left, + (TextAlignKeyword::Start, false) => TextAlignKeyword::Right, + (TextAlignKeyword::End, true) => TextAlignKeyword::Right, + (TextAlignKeyword::End, false) => TextAlignKeyword::Left, + _ => parent, + } + }, + #[cfg(feature = "gecko")] + TextAlign::MozCenterOrInherit => { + let parent = _context + .builder + .get_parent_inherited_text() + .clone_text_align(); + if parent == TextAlignKeyword::Start { + TextAlignKeyword::Center + } else { + parent + } + }, + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + TextAlign::Keyword(*computed) + } +} + +fn fill_mode_is_default_and_shape_exists( + fill: &TextEmphasisFillMode, + shape: &Option<TextEmphasisShapeKeyword>, +) -> bool { + shape.is_some() && fill.is_filled() +} + +/// Specified value of text-emphasis-style property. +/// +/// https://drafts.csswg.org/css-text-decor/#propdef-text-emphasis-style +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +#[allow(missing_docs)] +pub enum TextEmphasisStyle { + /// [ <fill> || <shape> ] + Keyword { + #[css(contextual_skip_if = "fill_mode_is_default_and_shape_exists")] + fill: TextEmphasisFillMode, + shape: Option<TextEmphasisShapeKeyword>, + }, + /// `none` + None, + /// `<string>` (of which only the first grapheme cluster will be used). + String(crate::OwnedStr), +} + +/// Fill mode for the text-emphasis-style property +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum TextEmphasisFillMode { + /// `filled` + Filled, + /// `open` + Open, +} + +impl TextEmphasisFillMode { + /// Whether the value is `filled`. + #[inline] + pub fn is_filled(&self) -> bool { + matches!(*self, TextEmphasisFillMode::Filled) + } +} + +/// Shape keyword for the text-emphasis-style property +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum TextEmphasisShapeKeyword { + /// `dot` + Dot, + /// `circle` + Circle, + /// `double-circle` + DoubleCircle, + /// `triangle` + Triangle, + /// `sesame` + Sesame, +} + +impl ToComputedValue for TextEmphasisStyle { + type ComputedValue = ComputedTextEmphasisStyle; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + TextEmphasisStyle::Keyword { fill, shape } => { + let shape = shape.unwrap_or_else(|| { + // FIXME(emilio, bug 1572958): This should set the + // rule_cache_conditions properly. + // + // Also should probably use WritingMode::is_vertical rather + // than the computed value of the `writing-mode` property. + if context.style().get_inherited_box().clone_writing_mode() == + SpecifiedWritingMode::HorizontalTb + { + TextEmphasisShapeKeyword::Circle + } else { + TextEmphasisShapeKeyword::Sesame + } + }); + ComputedTextEmphasisStyle::Keyword { fill, shape } + }, + TextEmphasisStyle::None => ComputedTextEmphasisStyle::None, + TextEmphasisStyle::String(ref s) => { + // Passing `true` to iterate over extended grapheme clusters, following + // recommendation at http://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries + // + // FIXME(emilio): Doing this at computed value time seems wrong. + // The spec doesn't say that this should be a computed-value + // time operation. This is observable from getComputedStyle(). + let s = s.graphemes(true).next().unwrap_or("").to_string(); + ComputedTextEmphasisStyle::String(s.into()) + }, + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + match *computed { + ComputedTextEmphasisStyle::Keyword { fill, shape } => TextEmphasisStyle::Keyword { + fill, + shape: Some(shape), + }, + ComputedTextEmphasisStyle::None => TextEmphasisStyle::None, + ComputedTextEmphasisStyle::String(ref string) => { + TextEmphasisStyle::String(string.clone()) + }, + } + } +} + +impl Parse for TextEmphasisStyle { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("none")) + .is_ok() + { + return Ok(TextEmphasisStyle::None); + } + + if let Ok(s) = input.try_parse(|i| i.expect_string().map(|s| s.as_ref().to_owned())) { + // Handle <string> + return Ok(TextEmphasisStyle::String(s.into())); + } + + // Handle a pair of keywords + let mut shape = input.try_parse(TextEmphasisShapeKeyword::parse).ok(); + let fill = input.try_parse(TextEmphasisFillMode::parse).ok(); + if shape.is_none() { + shape = input.try_parse(TextEmphasisShapeKeyword::parse).ok(); + } + + if shape.is_none() && fill.is_none() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + // If a shape keyword is specified but neither filled nor open is + // specified, filled is assumed. + let fill = fill.unwrap_or(TextEmphasisFillMode::Filled); + + // We cannot do the same because the default `<shape>` depends on the + // computed writing-mode. + Ok(TextEmphasisStyle::Keyword { fill, shape }) + } +} + +/// The allowed horizontal values for the `text-emphasis-position` property. +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub enum TextEmphasisHorizontalWritingModeValue { + /// Draw marks over the text in horizontal writing mode. + Over, + /// Draw marks under the text in horizontal writing mode. + Under, +} + +/// The allowed vertical values for the `text-emphasis-position` property. +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub enum TextEmphasisVerticalWritingModeValue { + /// Draws marks to the right of the text in vertical writing mode. + Right, + /// Draw marks to the left of the text in vertical writing mode. + Left, +} + +/// Specified value of `text-emphasis-position` property. +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub struct TextEmphasisPosition( + pub TextEmphasisHorizontalWritingModeValue, + pub TextEmphasisVerticalWritingModeValue, +); + +impl TextEmphasisPosition { + #[inline] + /// Returns the initial value of `text-emphasis-position` + pub fn over_right() -> Self { + TextEmphasisPosition( + TextEmphasisHorizontalWritingModeValue::Over, + TextEmphasisVerticalWritingModeValue::Right, + ) + } + + #[cfg(feature = "gecko")] + /// Converts an enumerated value coming from Gecko to a `TextEmphasisPosition`. + pub fn from_gecko_keyword(kw: u32) -> Self { + use crate::gecko_bindings::structs; + + let vert = if kw & structs::NS_STYLE_TEXT_EMPHASIS_POSITION_RIGHT != 0 { + TextEmphasisVerticalWritingModeValue::Right + } else { + debug_assert!(kw & structs::NS_STYLE_TEXT_EMPHASIS_POSITION_LEFT != 0); + TextEmphasisVerticalWritingModeValue::Left + }; + let horiz = if kw & structs::NS_STYLE_TEXT_EMPHASIS_POSITION_OVER != 0 { + TextEmphasisHorizontalWritingModeValue::Over + } else { + debug_assert!(kw & structs::NS_STYLE_TEXT_EMPHASIS_POSITION_UNDER != 0); + TextEmphasisHorizontalWritingModeValue::Under + }; + TextEmphasisPosition(horiz, vert) + } +} + +impl Parse for TextEmphasisPosition { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(horizontal) = + input.try_parse(|input| TextEmphasisHorizontalWritingModeValue::parse(input)) + { + let vertical = TextEmphasisVerticalWritingModeValue::parse(input)?; + Ok(TextEmphasisPosition(horizontal, vertical)) + } else { + let vertical = TextEmphasisVerticalWritingModeValue::parse(input)?; + let horizontal = TextEmphasisHorizontalWritingModeValue::parse(input)?; + Ok(TextEmphasisPosition(horizontal, vertical)) + } + } +} + +#[cfg(feature = "gecko")] +impl From<u8> for TextEmphasisPosition { + fn from(bits: u8) -> Self { + TextEmphasisPosition::from_gecko_keyword(bits as u32) + } +} + +#[cfg(feature = "gecko")] +impl From<TextEmphasisPosition> for u8 { + fn from(v: TextEmphasisPosition) -> u8 { + use crate::gecko_bindings::structs; + + let mut result = match v.0 { + TextEmphasisHorizontalWritingModeValue::Over => { + structs::NS_STYLE_TEXT_EMPHASIS_POSITION_OVER + }, + TextEmphasisHorizontalWritingModeValue::Under => { + structs::NS_STYLE_TEXT_EMPHASIS_POSITION_UNDER + }, + }; + match v.1 { + TextEmphasisVerticalWritingModeValue::Right => { + result |= structs::NS_STYLE_TEXT_EMPHASIS_POSITION_RIGHT; + }, + TextEmphasisVerticalWritingModeValue::Left => { + result |= structs::NS_STYLE_TEXT_EMPHASIS_POSITION_LEFT; + }, + }; + result as u8 + } +} + +/// Values for the `word-break` property. +#[repr(u8)] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +pub enum WordBreak { + Normal, + BreakAll, + KeepAll, + /// The break-word value, needed for compat. + /// + /// Specifying `word-break: break-word` makes `overflow-wrap` behave as + /// `anywhere`, and `word-break` behave like `normal`. + #[cfg(feature = "gecko")] + BreakWord, +} + +/// Values for the `line-break` property. +#[repr(u8)] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +pub enum LineBreak { + Auto, + Loose, + Normal, + Strict, + Anywhere, +} + +/// Values for the `overflow-wrap` property. +#[repr(u8)] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +pub enum OverflowWrap { + Normal, + BreakWord, + Anywhere, +} + +/// Implements text-decoration-skip-ink which takes the keywords auto | none | all +/// +/// https://drafts.csswg.org/css-text-decor-4/#text-decoration-skip-ink-property +#[repr(u8)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +pub enum TextDecorationSkipInk { + Auto, + None, + All, +} + +/// Implements type for `text-decoration-thickness` property +pub type TextDecorationLength = GenericTextDecorationLength<LengthPercentage>; + +impl TextDecorationLength { + /// `Auto` value. + #[inline] + pub fn auto() -> Self { + GenericTextDecorationLength::Auto + } + + /// Whether this is the `Auto` value. + #[inline] + pub fn is_auto(&self) -> bool { + matches!(*self, GenericTextDecorationLength::Auto) + } +} + +bitflags! { + #[derive(MallocSizeOf, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem)] + #[value_info(other_values = "auto,from-font,under,left,right")] + #[repr(C)] + /// Specified keyword values for the text-underline-position property. + /// (Non-exclusive, but not all combinations are allowed: the spec grammar gives + /// `auto | [ from-font | under ] || [ left | right ]`.) + /// https://drafts.csswg.org/css-text-decor-4/#text-underline-position-property + pub struct TextUnderlinePosition: u8 { + /// Use automatic positioning below the alphabetic baseline. + const AUTO = 0; + /// Use underline position from the first available font. + const FROM_FONT = 1 << 0; + /// Below the glyph box. + const UNDER = 1 << 1; + /// In vertical mode, place to the left of the text. + const LEFT = 1 << 2; + /// In vertical mode, place to the right of the text. + const RIGHT = 1 << 3; + } +} + +impl Parse for TextUnderlinePosition { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<TextUnderlinePosition, ParseError<'i>> { + let mut result = TextUnderlinePosition::empty(); + + loop { + let location = input.current_source_location(); + let ident = match input.next() { + Ok(&Token::Ident(ref ident)) => ident, + Ok(other) => return Err(location.new_unexpected_token_error(other.clone())), + Err(..) => break, + }; + + match_ignore_ascii_case! { ident, + "auto" if result.is_empty() => { + return Ok(result); + }, + "from-font" if !result.intersects(TextUnderlinePosition::FROM_FONT | + TextUnderlinePosition::UNDER) => { + result.insert(TextUnderlinePosition::FROM_FONT); + }, + "under" if !result.intersects(TextUnderlinePosition::FROM_FONT | + TextUnderlinePosition::UNDER) => { + result.insert(TextUnderlinePosition::UNDER); + }, + "left" if !result.intersects(TextUnderlinePosition::LEFT | + TextUnderlinePosition::RIGHT) => { + result.insert(TextUnderlinePosition::LEFT); + }, + "right" if !result.intersects(TextUnderlinePosition::LEFT | + TextUnderlinePosition::RIGHT) => { + result.insert(TextUnderlinePosition::RIGHT); + }, + _ => return Err(location.new_custom_error( + SelectorParseErrorKind::UnexpectedIdent(ident.clone()) + )), + } + } + + if !result.is_empty() { + Ok(result) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +impl ToCss for TextUnderlinePosition { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.is_empty() { + return dest.write_str("auto"); + } + + let mut writer = SequenceWriter::new(dest, " "); + let mut any = false; + + macro_rules! maybe_write { + ($ident:ident => $str:expr) => { + if self.contains(TextUnderlinePosition::$ident) { + any = true; + writer.raw_item($str)?; + } + }; + } + + maybe_write!(FROM_FONT => "from-font"); + maybe_write!(UNDER => "under"); + maybe_write!(LEFT => "left"); + maybe_write!(RIGHT => "right"); + + debug_assert!(any); + + Ok(()) + } +} diff --git a/servo/components/style/values/specified/time.rs b/servo/components/style/values/specified/time.rs new file mode 100644 index 0000000000..aba3f0a828 --- /dev/null +++ b/servo/components/style/values/specified/time.rs @@ -0,0 +1,176 @@ +/* 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/. */ + +//! Specified time values. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::time::Time as ComputedTime; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::specified::calc::CalcNode; +use crate::values::CSSFloat; +use cssparser::{Parser, Token}; +use std::fmt::{self, Write}; +use style_traits::values::specified::AllowedNumericType; +use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss}; + +/// A time value according to CSS-VALUES § 6.2. +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub struct Time { + seconds: CSSFloat, + unit: TimeUnit, + was_calc: bool, +} + +/// A time unit. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] +pub enum TimeUnit { + /// `s` + Second, + /// `ms` + Millisecond, +} + +impl Time { + /// Returns a time value that represents `seconds` seconds. + pub fn from_seconds(seconds: CSSFloat) -> Self { + Time { + seconds, + unit: TimeUnit::Second, + was_calc: false, + } + } + + /// Returns `0s`. + pub fn zero() -> Self { + Self::from_seconds(0.0) + } + + /// Returns the time in fractional seconds. + pub fn seconds(self) -> CSSFloat { + self.seconds + } + + /// Parses a time according to CSS-VALUES § 6.2. + pub fn parse_dimension(value: CSSFloat, unit: &str, was_calc: bool) -> Result<Time, ()> { + let (seconds, unit) = match_ignore_ascii_case! { unit, + "s" => (value, TimeUnit::Second), + "ms" => (value / 1000.0, TimeUnit::Millisecond), + _ => return Err(()) + }; + + Ok(Time { + seconds, + unit, + was_calc, + }) + } + + /// Returns a `Time` value from a CSS `calc()` expression. + pub fn from_calc(seconds: CSSFloat) -> Self { + Time { + seconds, + unit: TimeUnit::Second, + was_calc: true, + } + } + + fn parse_with_clamping_mode<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + clamping_mode: AllowedNumericType, + ) -> Result<Self, ParseError<'i>> { + use style_traits::ParsingMode; + + let location = input.current_source_location(); + match *input.next()? { + // Note that we generally pass ParserContext to is_ok() to check + // that the ParserMode of the ParserContext allows all numeric + // values for SMIL regardless of clamping_mode, but in this Time + // value case, the value does not animate for SMIL at all, so we use + // ParsingMode::DEFAULT directly. + Token::Dimension { + value, ref unit, .. + } if clamping_mode.is_ok(ParsingMode::DEFAULT, value) => { + Time::parse_dimension(value, unit, /* from_calc = */ false) + .map_err(|()| location.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + }, + Token::Function(ref name) => { + let function = CalcNode::math_function(name, location)?; + let time = CalcNode::parse_time(context, input, function)?; + + // FIXME(emilio): Rejecting calc() at parse time is wrong, + // was_calc should probably be replaced by calc_clamping_mode or + // something like we do for numbers, or we should do the + // clamping here instead (simpler, but technically incorrect, + // though still more correct than this!). + if !clamping_mode.is_ok(ParsingMode::DEFAULT, time.seconds) { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(time) + }, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + } + } + + /// Parses a non-negative time value. + pub fn parse_non_negative<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with_clamping_mode(context, input, AllowedNumericType::NonNegative) + } +} + +impl ToComputedValue for Time { + type ComputedValue = ComputedTime; + + fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue { + ComputedTime::from_seconds(self.seconds()) + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Time { + seconds: computed.seconds(), + unit: TimeUnit::Second, + was_calc: false, + } + } +} + +impl Parse for Time { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with_clamping_mode(context, input, AllowedNumericType::All) + } +} + +impl ToCss for Time { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.was_calc { + dest.write_str("calc(")?; + } + match self.unit { + TimeUnit::Second => { + self.seconds.to_css(dest)?; + dest.write_str("s")?; + }, + TimeUnit::Millisecond => { + (self.seconds * 1000.).to_css(dest)?; + dest.write_str("ms")?; + }, + } + if self.was_calc { + dest.write_str(")")?; + } + Ok(()) + } +} + +impl SpecifiedValueInfo for Time {} diff --git a/servo/components/style/values/specified/transform.rs b/servo/components/style/values/specified/transform.rs new file mode 100644 index 0000000000..ba2be4ee8d --- /dev/null +++ b/servo/components/style/values/specified/transform.rs @@ -0,0 +1,481 @@ +/* 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/. */ + +//! Specified types for CSS values that are related to transformations. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::{Context, LengthPercentage as ComputedLengthPercentage}; +use crate::values::computed::{Percentage as ComputedPercentage, ToComputedValue}; +use crate::values::generics::transform as generic; +use crate::values::generics::transform::{Matrix, Matrix3D}; +use crate::values::specified::position::{ + HorizontalPositionKeyword, Side, VerticalPositionKeyword, +}; +use crate::values::specified::{ + self, Angle, Integer, Length, LengthPercentage, Number, NumberOrPercentage, +}; +use crate::Zero; +use cssparser::Parser; +use style_traits::{ParseError, StyleParseErrorKind}; + +pub use crate::values::generics::transform::TransformStyle; + +/// A single operation in a specified CSS `transform` +pub type TransformOperation = + generic::TransformOperation<Angle, Number, Length, Integer, LengthPercentage>; + +/// A specified CSS `transform` +pub type Transform = generic::Transform<TransformOperation>; + +/// The specified value of a CSS `<transform-origin>` +pub type TransformOrigin = generic::TransformOrigin< + OriginComponent<HorizontalPositionKeyword>, + OriginComponent<VerticalPositionKeyword>, + Length, +>; + +impl TransformOrigin { + /// Returns the initial specified value for `transform-origin`. + #[inline] + pub fn initial_value() -> Self { + Self::new( + OriginComponent::Length(LengthPercentage::Percentage(ComputedPercentage(0.5))), + OriginComponent::Length(LengthPercentage::Percentage(ComputedPercentage(0.5))), + Length::zero(), + ) + } + + /// Returns the `0 0` value. + pub fn zero_zero() -> Self { + Self::new( + OriginComponent::Length(LengthPercentage::zero()), + OriginComponent::Length(LengthPercentage::zero()), + Length::zero(), + ) + } +} + +impl Transform { + /// Internal parse function for deciding if we wish to accept prefixed values or not + /// + /// `transform` allows unitless zero angles as an exception, see: + /// https://github.com/w3c/csswg-drafts/issues/1162 + fn parse_internal<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + use style_traits::{Separator, Space}; + + if input + .try_parse(|input| input.expect_ident_matching("none")) + .is_ok() + { + return Ok(generic::Transform::none()); + } + + Ok(generic::Transform( + Space::parse(input, |input| { + let function = input.expect_function()?.clone(); + input.parse_nested_block(|input| { + let location = input.current_source_location(); + let result = match_ignore_ascii_case! { &function, + "matrix" => { + let a = Number::parse(context, input)?; + input.expect_comma()?; + let b = Number::parse(context, input)?; + input.expect_comma()?; + let c = Number::parse(context, input)?; + input.expect_comma()?; + let d = Number::parse(context, input)?; + input.expect_comma()?; + // Standard matrix parsing. + let e = Number::parse(context, input)?; + input.expect_comma()?; + let f = Number::parse(context, input)?; + Ok(generic::TransformOperation::Matrix(Matrix { a, b, c, d, e, f })) + }, + "matrix3d" => { + let m11 = Number::parse(context, input)?; + input.expect_comma()?; + let m12 = Number::parse(context, input)?; + input.expect_comma()?; + let m13 = Number::parse(context, input)?; + input.expect_comma()?; + let m14 = Number::parse(context, input)?; + input.expect_comma()?; + let m21 = Number::parse(context, input)?; + input.expect_comma()?; + let m22 = Number::parse(context, input)?; + input.expect_comma()?; + let m23 = Number::parse(context, input)?; + input.expect_comma()?; + let m24 = Number::parse(context, input)?; + input.expect_comma()?; + let m31 = Number::parse(context, input)?; + input.expect_comma()?; + let m32 = Number::parse(context, input)?; + input.expect_comma()?; + let m33 = Number::parse(context, input)?; + input.expect_comma()?; + let m34 = Number::parse(context, input)?; + input.expect_comma()?; + // Standard matrix3d parsing. + let m41 = Number::parse(context, input)?; + input.expect_comma()?; + let m42 = Number::parse(context, input)?; + input.expect_comma()?; + let m43 = Number::parse(context, input)?; + input.expect_comma()?; + let m44 = Number::parse(context, input)?; + Ok(generic::TransformOperation::Matrix3D(Matrix3D { + m11, m12, m13, m14, + m21, m22, m23, m24, + m31, m32, m33, m34, + m41, m42, m43, m44, + })) + }, + "translate" => { + let sx = specified::LengthPercentage::parse(context, input)?; + if input.try_parse(|input| input.expect_comma()).is_ok() { + let sy = specified::LengthPercentage::parse(context, input)?; + Ok(generic::TransformOperation::Translate(sx, sy)) + } else { + Ok(generic::TransformOperation::Translate(sx, Zero::zero())) + } + }, + "translatex" => { + let tx = specified::LengthPercentage::parse(context, input)?; + Ok(generic::TransformOperation::TranslateX(tx)) + }, + "translatey" => { + let ty = specified::LengthPercentage::parse(context, input)?; + Ok(generic::TransformOperation::TranslateY(ty)) + }, + "translatez" => { + let tz = specified::Length::parse(context, input)?; + Ok(generic::TransformOperation::TranslateZ(tz)) + }, + "translate3d" => { + let tx = specified::LengthPercentage::parse(context, input)?; + input.expect_comma()?; + let ty = specified::LengthPercentage::parse(context, input)?; + input.expect_comma()?; + let tz = specified::Length::parse(context, input)?; + Ok(generic::TransformOperation::Translate3D(tx, ty, tz)) + }, + "scale" => { + let sx = NumberOrPercentage::parse(context, input)?.to_number(); + if input.try_parse(|input| input.expect_comma()).is_ok() { + let sy = NumberOrPercentage::parse(context, input)?.to_number(); + Ok(generic::TransformOperation::Scale(sx, sy)) + } else { + Ok(generic::TransformOperation::Scale(sx, sx)) + } + }, + "scalex" => { + let sx = NumberOrPercentage::parse(context, input)?.to_number(); + Ok(generic::TransformOperation::ScaleX(sx)) + }, + "scaley" => { + let sy = NumberOrPercentage::parse(context, input)?.to_number(); + Ok(generic::TransformOperation::ScaleY(sy)) + }, + "scalez" => { + let sz = NumberOrPercentage::parse(context, input)?.to_number(); + Ok(generic::TransformOperation::ScaleZ(sz)) + }, + "scale3d" => { + let sx = NumberOrPercentage::parse(context, input)?.to_number(); + input.expect_comma()?; + let sy = NumberOrPercentage::parse(context, input)?.to_number(); + input.expect_comma()?; + let sz = NumberOrPercentage::parse(context, input)?.to_number(); + Ok(generic::TransformOperation::Scale3D(sx, sy, sz)) + }, + "rotate" => { + let theta = specified::Angle::parse_with_unitless(context, input)?; + Ok(generic::TransformOperation::Rotate(theta)) + }, + "rotatex" => { + let theta = specified::Angle::parse_with_unitless(context, input)?; + Ok(generic::TransformOperation::RotateX(theta)) + }, + "rotatey" => { + let theta = specified::Angle::parse_with_unitless(context, input)?; + Ok(generic::TransformOperation::RotateY(theta)) + }, + "rotatez" => { + let theta = specified::Angle::parse_with_unitless(context, input)?; + Ok(generic::TransformOperation::RotateZ(theta)) + }, + "rotate3d" => { + let ax = Number::parse(context, input)?; + input.expect_comma()?; + let ay = Number::parse(context, input)?; + input.expect_comma()?; + let az = Number::parse(context, input)?; + input.expect_comma()?; + let theta = specified::Angle::parse_with_unitless(context, input)?; + // TODO(gw): Check that the axis can be normalized. + Ok(generic::TransformOperation::Rotate3D(ax, ay, az, theta)) + }, + "skew" => { + let ax = specified::Angle::parse_with_unitless(context, input)?; + if input.try_parse(|input| input.expect_comma()).is_ok() { + let ay = specified::Angle::parse_with_unitless(context, input)?; + Ok(generic::TransformOperation::Skew(ax, ay)) + } else { + Ok(generic::TransformOperation::Skew(ax, Zero::zero())) + } + }, + "skewx" => { + let theta = specified::Angle::parse_with_unitless(context, input)?; + Ok(generic::TransformOperation::SkewX(theta)) + }, + "skewy" => { + let theta = specified::Angle::parse_with_unitless(context, input)?; + Ok(generic::TransformOperation::SkewY(theta)) + }, + "perspective" => { + let d = specified::Length::parse_non_negative(context, input)?; + Ok(generic::TransformOperation::Perspective(d)) + }, + _ => Err(()), + }; + result.map_err(|()| { + location.new_custom_error(StyleParseErrorKind::UnexpectedFunction( + function.clone(), + )) + }) + }) + })? + .into(), + )) + } +} + +impl Parse for Transform { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Transform::parse_internal(context, input) + } +} + +/// The specified value of a component of a CSS `<transform-origin>`. +#[derive(Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum OriginComponent<S> { + /// `center` + Center, + /// `<length-percentage>` + Length(LengthPercentage), + /// `<side>` + Side(S), +} + +impl Parse for TransformOrigin { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let parse_depth = |input: &mut Parser| { + input + .try_parse(|i| Length::parse(context, i)) + .unwrap_or(Length::zero()) + }; + match input.try_parse(|i| OriginComponent::parse(context, i)) { + Ok(x_origin @ OriginComponent::Center) => { + if let Ok(y_origin) = input.try_parse(|i| OriginComponent::parse(context, i)) { + let depth = parse_depth(input); + return Ok(Self::new(x_origin, y_origin, depth)); + } + let y_origin = OriginComponent::Center; + if let Ok(x_keyword) = input.try_parse(HorizontalPositionKeyword::parse) { + let x_origin = OriginComponent::Side(x_keyword); + let depth = parse_depth(input); + return Ok(Self::new(x_origin, y_origin, depth)); + } + let depth = Length::from_px(0.); + return Ok(Self::new(x_origin, y_origin, depth)); + }, + Ok(x_origin) => { + if let Ok(y_origin) = input.try_parse(|i| OriginComponent::parse(context, i)) { + let depth = parse_depth(input); + return Ok(Self::new(x_origin, y_origin, depth)); + } + let y_origin = OriginComponent::Center; + let depth = Length::from_px(0.); + return Ok(Self::new(x_origin, y_origin, depth)); + }, + Err(_) => {}, + } + let y_keyword = VerticalPositionKeyword::parse(input)?; + let y_origin = OriginComponent::Side(y_keyword); + if let Ok(x_keyword) = input.try_parse(HorizontalPositionKeyword::parse) { + let x_origin = OriginComponent::Side(x_keyword); + let depth = parse_depth(input); + return Ok(Self::new(x_origin, y_origin, depth)); + } + if input + .try_parse(|i| i.expect_ident_matching("center")) + .is_ok() + { + let x_origin = OriginComponent::Center; + let depth = parse_depth(input); + return Ok(Self::new(x_origin, y_origin, depth)); + } + let x_origin = OriginComponent::Center; + let depth = Length::from_px(0.); + Ok(Self::new(x_origin, y_origin, depth)) + } +} + +impl<S> ToComputedValue for OriginComponent<S> +where + S: Side, +{ + type ComputedValue = ComputedLengthPercentage; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + OriginComponent::Center => { + ComputedLengthPercentage::new_percent(ComputedPercentage(0.5)) + }, + OriginComponent::Length(ref length) => length.to_computed_value(context), + OriginComponent::Side(ref keyword) => { + let p = ComputedPercentage(if keyword.is_start() { 0. } else { 1. }); + ComputedLengthPercentage::new_percent(p) + }, + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + OriginComponent::Length(ToComputedValue::from_computed_value(computed)) + } +} + +impl<S> OriginComponent<S> { + /// `0%` + pub fn zero() -> Self { + OriginComponent::Length(LengthPercentage::Percentage(ComputedPercentage::zero())) + } +} + +/// A specified CSS `rotate` +pub type Rotate = generic::Rotate<Number, Angle>; + +impl Parse for Rotate { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(generic::Rotate::None); + } + + // Parse <angle> or [ x | y | z | <number>{3} ] && <angle>. + // + // The rotate axis and angle could be in any order, so we parse angle twice to cover + // two cases. i.e. `<number>{3} <angle>` or `<angle> <number>{3}` + let angle = input + .try_parse(|i| specified::Angle::parse(context, i)) + .ok(); + let axis = input + .try_parse(|i| { + Ok(try_match_ident_ignore_ascii_case! { i, + "x" => (Number::new(1.), Number::new(0.), Number::new(0.)), + "y" => (Number::new(0.), Number::new(1.), Number::new(0.)), + "z" => (Number::new(0.), Number::new(0.), Number::new(1.)), + }) + }) + .or_else(|_: ParseError| -> Result<_, ParseError> { + input.try_parse(|i| { + Ok(( + Number::parse(context, i)?, + Number::parse(context, i)?, + Number::parse(context, i)?, + )) + }) + }) + .ok(); + let angle = match angle { + Some(a) => a, + None => specified::Angle::parse(context, input)?, + }; + + Ok(match axis { + Some((x, y, z)) => generic::Rotate::Rotate3D(x, y, z, angle), + None => generic::Rotate::Rotate(angle), + }) + } +} + +/// A specified CSS `translate` +pub type Translate = generic::Translate<LengthPercentage, Length>; + +impl Parse for Translate { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(generic::Translate::None); + } + + let tx = specified::LengthPercentage::parse(context, input)?; + if let Ok(ty) = input.try_parse(|i| specified::LengthPercentage::parse(context, i)) { + if let Ok(tz) = input.try_parse(|i| specified::Length::parse(context, i)) { + // 'translate: <length-percentage> <length-percentage> <length>' + return Ok(generic::Translate::Translate(tx, ty, tz)); + } + + // translate: <length-percentage> <length-percentage>' + return Ok(generic::Translate::Translate( + tx, + ty, + specified::Length::zero(), + )); + } + + // 'translate: <length-percentage> ' + Ok(generic::Translate::Translate( + tx, + specified::LengthPercentage::zero(), + specified::Length::zero(), + )) + } +} + +/// A specified CSS `scale` +pub type Scale = generic::Scale<Number>; + +impl Parse for Scale { + /// Scale accepts <number> | <percentage>, so we parse it as NumberOrPercentage, + /// and then convert into an Number if it's a Percentage. + /// https://github.com/w3c/csswg-drafts/pull/4396 + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(generic::Scale::None); + } + + let sx = NumberOrPercentage::parse(context, input)?.to_number(); + if let Ok(sy) = input.try_parse(|i| NumberOrPercentage::parse(context, i)) { + let sy = sy.to_number(); + if let Ok(sz) = input.try_parse(|i| NumberOrPercentage::parse(context, i)) { + // 'scale: <number> <number> <number>' + return Ok(generic::Scale::Scale(sx, sy, sz.to_number())); + } + + // 'scale: <number> <number>' + return Ok(generic::Scale::Scale(sx, sy, Number::new(1.0))); + } + + // 'scale: <number>' + Ok(generic::Scale::Scale(sx, sx, Number::new(1.0))) + } +} diff --git a/servo/components/style/values/specified/ui.rs b/servo/components/style/values/specified/ui.rs new file mode 100644 index 0000000000..74440a6720 --- /dev/null +++ b/servo/components/style/values/specified/ui.rs @@ -0,0 +1,238 @@ +/* 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/. */ + +//! Specified types for UI properties. + +use crate::parser::{Parse, ParserContext}; +use crate::values::generics::ui as generics; +use crate::values::specified::color::Color; +use crate::values::specified::url::SpecifiedImageUrl; +use crate::values::specified::Number; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +/// A specified value for the `cursor` property. +pub type Cursor = generics::GenericCursor<CursorImage>; + +/// A specified value for item of `image cursors`. +pub type CursorImage = generics::GenericCursorImage<SpecifiedImageUrl, Number>; + +impl Parse for Cursor { + /// cursor: [<url> [<number> <number>]?]# [auto | default | ...] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut images = vec![]; + loop { + match input.try_parse(|input| CursorImage::parse(context, input)) { + Ok(image) => images.push(image), + Err(_) => break, + } + input.expect_comma()?; + } + Ok(Self { + images: images.into(), + keyword: CursorKind::parse(input)?, + }) + } +} + +impl Parse for CursorImage { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + use crate::Zero; + + let url = SpecifiedImageUrl::parse(context, input)?; + let mut has_hotspot = false; + let mut hotspot_x = Number::zero(); + let mut hotspot_y = Number::zero(); + + if let Ok(x) = input.try_parse(|input| Number::parse(context, input)) { + has_hotspot = true; + hotspot_x = x; + hotspot_y = Number::parse(context, input)?; + } + + Ok(Self { + url, + has_hotspot, + hotspot_x, + hotspot_y, + }) + } +} + +/// Specified value of `-moz-force-broken-image-icon` +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +pub struct MozForceBrokenImageIcon(pub bool); + +impl MozForceBrokenImageIcon { + /// Return initial value of -moz-force-broken-image-icon which is false. + #[inline] + pub fn false_value() -> MozForceBrokenImageIcon { + MozForceBrokenImageIcon(false) + } +} + +impl Parse for MozForceBrokenImageIcon { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<MozForceBrokenImageIcon, ParseError<'i>> { + // We intentionally don't support calc values here. + match input.expect_integer()? { + 0 => Ok(MozForceBrokenImageIcon(false)), + 1 => Ok(MozForceBrokenImageIcon(true)), + _ => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } + } +} + +impl ToCss for MozForceBrokenImageIcon { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str(if self.0 { "1" } else { "0" }) + } +} + +impl From<u8> for MozForceBrokenImageIcon { + fn from(bits: u8) -> MozForceBrokenImageIcon { + MozForceBrokenImageIcon(bits == 1) + } +} + +impl From<MozForceBrokenImageIcon> for u8 { + fn from(v: MozForceBrokenImageIcon) -> u8 { + if v.0 { + 1 + } else { + 0 + } + } +} + +/// A specified value for `scrollbar-color` property +pub type ScrollbarColor = generics::ScrollbarColor<Color>; + +impl Parse for ScrollbarColor { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() { + return Ok(generics::ScrollbarColor::Auto); + } + Ok(generics::ScrollbarColor::Colors { + thumb: Color::parse(context, input)?, + track: Color::parse(context, input)?, + }) + } +} + +/// The specified value for the `user-select` property. +/// +/// https://drafts.csswg.org/css-ui-4/#propdef-user-select +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum UserSelect { + Auto, + Text, + #[parse(aliases = "-moz-none")] + None, + /// Force selection of all children. + All, +} + +/// The keywords allowed in the Cursor property. +/// +/// https://drafts.csswg.org/css-ui-4/#propdef-cursor +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum CursorKind { + None, + Default, + Pointer, + ContextMenu, + Help, + Progress, + Wait, + Cell, + Crosshair, + Text, + VerticalText, + Alias, + Copy, + Move, + NoDrop, + NotAllowed, + #[parse(aliases = "-moz-grab")] + Grab, + #[parse(aliases = "-moz-grabbing")] + Grabbing, + EResize, + NResize, + NeResize, + NwResize, + SResize, + SeResize, + SwResize, + WResize, + EwResize, + NsResize, + NeswResize, + NwseResize, + ColResize, + RowResize, + AllScroll, + #[parse(aliases = "-moz-zoom-in")] + ZoomIn, + #[parse(aliases = "-moz-zoom-out")] + ZoomOut, + Auto, +} diff --git a/servo/components/style/values/specified/url.rs b/servo/components/style/values/specified/url.rs new file mode 100644 index 0000000000..b0a69b362d --- /dev/null +++ b/servo/components/style/values/specified/url.rs @@ -0,0 +1,18 @@ +/* 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/. */ + +//! Common handling for the specified value CSS url() values. + +use crate::values::generics::url::GenericUrlOrNone; + +#[cfg(feature = "gecko")] +pub use crate::gecko::url::{SpecifiedImageUrl, SpecifiedUrl}; +#[cfg(feature = "servo")] +pub use crate::servo::url::{SpecifiedImageUrl, SpecifiedUrl}; + +/// Specified <url> | <none> +pub type UrlOrNone = GenericUrlOrNone<SpecifiedUrl>; + +/// Specified image <url> | <none> +pub type ImageUrlOrNone = GenericUrlOrNone<SpecifiedImageUrl>; |