/* 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}; use crate::context::QuirksMode; use crate::parser::{Parse, ParserContext}; use crate::values::specified::calc::CalcNode; use crate::values::{serialize_atom_identifier, serialize_number, AtomString}; use crate::{Atom, Namespace, One, Prefix, Zero}; use cssparser::{Parser, Token}; use std::fmt::{self, Write}; use std::ops::Add; use style_traits::values::specified::AllowedNumericType; use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss}; pub use self::align::{AlignContent, AlignItems, AlignSelf, ContentDistribution}; pub use self::align::{JustifyContent, JustifyItems, JustifySelf, SelfAlignment}; pub use self::angle::{AllowUnitlessZeroAngle, Angle}; pub use self::animation::{ AnimationComposition, AnimationDirection, AnimationDuration, AnimationFillMode, AnimationIterationCount, AnimationName, AnimationPlayState, AnimationTimeline, ScrollAxis, TimelineName, TransitionBehavior, TransitionProperty, ViewTimelineInset, ViewTransitionName, }; pub use self::background::{BackgroundRepeat, BackgroundSize}; pub use self::basic_shape::FillRule; pub use self::border::{ BorderCornerRadius, BorderImageRepeat, BorderImageSideWidth, BorderImageSlice, BorderImageWidth, BorderRadius, BorderSideWidth, BorderSpacing, BorderStyle, LineWidth, }; pub use self::box_::{ Appearance, BaselineSource, BreakBetween, BreakWithin, Clear, Contain, ContainIntrinsicSize, ContainerName, ContainerType, ContentVisibility, Display, Float, LineClamp, Overflow, OverflowAnchor, OverflowClipBox, OverscrollBehavior, Perspective, Resize, ScrollSnapAlign, ScrollSnapAxis, ScrollSnapStop, ScrollSnapStrictness, ScrollSnapType, ScrollbarGutter, TouchAction, VerticalAlign, WillChange, Zoom, }; pub use self::color::{ Color, ColorOrAuto, ColorPropertyValue, ColorScheme, ForcedColorAdjust, PrintColorAdjust, }; pub use self::column::ColumnCount; pub use self::counters::{Content, ContentItem, CounterIncrement, CounterReset, CounterSet}; pub use self::easing::TimingFunction; pub use self::effects::{BoxShadow, Filter, SimpleShadow}; pub use self::flex::FlexBasis; pub use self::font::{FontFamily, FontLanguageOverride, FontPalette, FontStyle}; pub use self::font::{FontFeatureSettings, FontVariantLigatures, FontVariantNumeric}; pub use self::font::{ FontSize, FontSizeAdjust, FontSizeAdjustFactor, FontSizeKeyword, FontStretch, FontSynthesis, }; pub use self::font::{FontVariantAlternates, FontWeight}; pub use self::font::{FontVariantEastAsian, FontVariationSettings, LineHeight}; pub use self::font::{MathDepth, MozScriptMinSize, MozScriptSizeMultiplier, XLang, XTextScale}; pub use self::image::{EndingShape as GradientEndingShape, Gradient, Image, ImageRendering}; pub use self::length::{AbsoluteLength, AnchorSizeFunction, CalcLengthPercentage, CharacterWidth}; pub use self::length::{FontRelativeLength, Length, LengthOrNumber, NonNegativeLengthOrNumber}; pub use self::length::{LengthOrAuto, LengthPercentage, LengthPercentageOrAuto}; pub use self::length::{Margin, MaxSize, Size}; pub use self::length::{NoCalcLength, ViewportPercentageLength, ViewportVariant}; pub use self::length::{ NonNegativeLength, NonNegativeLengthPercentage, NonNegativeLengthPercentageOrAuto, }; #[cfg(feature = "gecko")] pub use self::list::ListStyleType; pub use self::list::Quotes; pub use self::motion::{OffsetPath, OffsetPosition, OffsetRotate}; pub use self::outline::OutlineStyle; pub use self::page::{PageName, PageOrientation, PageSize, PageSizeOrientation, PaperSize}; pub use self::percentage::{NonNegativePercentage, Percentage}; pub use self::position::AnchorFunction; pub use self::position::AnchorName; pub use self::position::AnchorScope; pub use self::position::AspectRatio; pub use self::position::Inset; pub use self::position::PositionAnchor; pub use self::position::PositionTryFallbacks; pub use self::position::PositionTryOrder; pub use self::position::PositionVisibility; pub use self::position::{GridAutoFlow, GridTemplateAreas, Position, PositionOrAuto}; pub use self::position::{MasonryAutoFlow, MasonryItemOrder, MasonryPlacement}; pub use self::position::{PositionArea, PositionAreaKeyword}; pub use self::position::{PositionComponent, ZIndex}; pub use self::ratio::Ratio; pub use self::rect::NonNegativeLengthOrNumberRect; pub use self::resolution::Resolution; pub use self::svg::{DProperty, MozContextProperties}; pub use self::svg::{SVGLength, SVGOpacity, SVGPaint}; pub use self::svg::{SVGPaintOrder, SVGStrokeDashArray, SVGWidth, VectorEffect}; pub use self::svg_path::SVGPathData; pub use self::text::HyphenateCharacter; pub use self::text::RubyPosition; pub use self::text::TextAlignLast; pub use self::text::TextUnderlinePosition; pub use self::text::{InitialLetter, LetterSpacing, LineBreak, TextAlign, TextIndent}; pub use self::text::{OverflowWrap, TextEmphasisPosition, TextEmphasisStyle, WordBreak}; pub use self::text::{TextAlignKeyword, TextDecorationLine, TextOverflow, WordSpacing}; pub use self::text::{TextDecorationLength, TextDecorationSkipInk, TextJustify, TextTransform}; pub use self::time::Time; pub use self::transform::{Rotate, Scale, Transform}; pub use self::transform::{TransformBox, TransformOrigin, TransformStyle, Translate}; #[cfg(feature = "gecko")] pub use self::ui::CursorImage; pub use self::ui::{ BoolInteger, Cursor, Inert, MozTheme, PointerEvents, ScrollbarColor, UserFocus, UserInput, UserModify, UserSelect, }; pub use super::generics::grid::GridTemplateComponent as GenericGridTemplateComponent; pub mod align; pub mod angle; pub mod animation; 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 page; pub mod percentage; pub mod position; pub mod ratio; pub mod rect; pub mod resolution; pub mod source_size_list; pub mod svg; pub mod svg_path; pub mod table; pub mod text; pub mod time; pub mod transform; pub mod ui; pub mod url; /// | /// 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> { 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> { AngleOrPercentage::parse_internal(context, input, AllowUnitlessZeroAngle::Yes) } } impl Parse for AngleOrPercentage { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { AngleOrPercentage::parse_internal(context, input, AllowUnitlessZeroAngle::No) } } /// Parse a `` 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> { let location = input.current_source_location(); match *input.next()? { Token::Number { value, .. } if clamping_mode.is_ok(context.parsing_mode, value) => { Ok(Number { value, calc_clamping_mode: None, }) }, Token::Function(ref name) => { let function = CalcNode::math_function(context, name, location)?; let value = CalcNode::parse_number(context, input, function)?; Ok(Number { value, calc_clamping_mode: Some(clamping_mode), }) }, ref t => Err(location.new_unexpected_token_error(t.clone())), } } /// A CSS `` specified value. /// /// https://drafts.csswg.org/css-values-3/#number-value #[derive(Clone, Copy, Debug, MallocSizeOf, 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, } impl Parse for Number { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { parse_number_with_clamping_mode(context, input, AllowedNumericType::All) } } impl PartialEq for Number { fn eq(&self, other: &Number) -> bool { if self.calc_clamping_mode != other.calc_clamping_mode { return false; } self.value == other.value || (self.value.is_nan() && other.value.is_nan()) } } impl Number { /// Returns a new number with the value `val`. #[inline] fn new_with_clamping_mode( value: CSSFloat, calc_clamping_mode: Option, ) -> 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`. #[inline] 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 { crate::values::normalize( self.calc_clamping_mode .map_or(self.value, |mode| mode.clamp(self.value)), ) .min(f32::MAX) .max(f32::MIN) } #[allow(missing_docs)] pub fn parse_non_negative<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { 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> { 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(&self, dest: &mut CssWriter) -> fmt::Result where W: Write, { serialize_number(self.value, self.calc_clamping_mode.is_some(), dest) } } 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 for f32 { #[inline] fn from(n: Number) -> Self { n.get() } } impl From for f64 { #[inline] fn from(n: Number) -> Self { n.get() as f64 } } /// A Number which is >= 0.0. pub type NonNegativeNumber = NonNegative; impl Parse for NonNegativeNumber { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { parse_number_with_clamping_mode(context, input, AllowedNumericType::NonNegative) .map(NonNegative::) } } 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::new(val.max(0.))) } /// Returns the numeric value. #[inline] pub fn get(&self) -> f32 { self.0.get() } } /// An Integer which is >= 0. pub type NonNegativeInteger = NonNegative; impl Parse for NonNegativeInteger { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { Ok(NonNegative(Integer::parse_non_negative(context, input)?)) } } /// A Number which is >= 1.0. pub type GreaterThanOrEqualToOneNumber = GreaterThanOrEqualToOne; impl Parse for GreaterThanOrEqualToOneNumber { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { parse_number_with_clamping_mode(context, input, AllowedNumericType::AtLeastOne) .map(GreaterThanOrEqualToOne::) } } /// | /// /// 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> { 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::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::parse_with_clamping_mode(context, input, AllowedNumericType::All) } } /// A non-negative | . pub type NonNegativeNumberOrPercentage = NonNegative; impl NonNegativeNumberOrPercentage { /// Returns the `100%` value. #[inline] pub fn hundred_percent() -> Self { NonNegative(NumberOrPercentage::Percentage(Percentage::hundred())) } /// Return a particular number. #[inline] pub fn new_number(n: f32) -> Self { NonNegative(NumberOrPercentage::Number(Number::new(n))) } } impl Parse for NonNegativeNumberOrPercentage { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { Ok(NonNegative(NumberOrPercentage::parse_non_negative( context, input, )?)) } } /// The value of Opacity is , which is " | ". /// 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 | , 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> { 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 ``, optionally coming from a `calc()` expression. /// /// #[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 PartialEq for Integer { fn eq(&self, value: &i32) -> bool { self.value() == *value } } 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> { 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(context, 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> { 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::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::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(&self, dest: &mut CssWriter) -> fmt::Result where W: Write, { if self.was_calc { dest.write_str("calc(")?; } self.value.to_css(dest)?; if self.was_calc { dest.write_char(')')?; } Ok(()) } } impl SpecifiedValueInfo for Integer {} /// A wrapper of Integer, with value >= 1. pub type PositiveInteger = GreaterThanOrEqualToOne; impl Parse for PositiveInteger { #[inline] fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { Integer::parse_positive(context, input).map(GreaterThanOrEqualToOne) } } /// The specified value of a grid `` pub type TrackBreadth = GenericTrackBreadth; /// The specified value of a grid `` pub type TrackSize = GenericTrackSize; /// The specified value of a grid `+` pub type ImplicitGridTracks = GenericImplicitGridTracks; /// The specified value of a grid `` /// (could also be `` or ``) pub type TrackList = GenericTrackList; /// The specified value of a ``. pub type GridLine = GenericGridLine; /// ` | ` pub type GridTemplateComponent = GenericGridTemplateComponent; /// rect(...) pub type ClipRect = generics::GenericClipRect; impl Parse for ClipRect { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { Self::parse_quirky(context, input, AllowQuirks::No) } } impl ClipRect { /// Parses a rect(, , , ), allowing quirks. fn parse_quirky<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, allow_quirks: AllowQuirks, ) -> Result> { input.expect_function_matching("rect")?; fn parse_argument<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, allow_quirks: AllowQuirks, ) -> Result> { 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; 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> { 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, /// Fallback value pub fallback: AtomString, } impl Parse for Attr { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { 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 { context.namespaces.prefixes.get(prefix).cloned() } /// Try to parse a namespace and return it if parsed, or none if there was not one present fn parse_namespace<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result<(Prefix, Namespace), ParseError<'i>> { let ns_prefix = match input.next()? { Token::Ident(ref prefix) => Some(Prefix::from(prefix.as_ref())), Token::Delim('|') => None, _ => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), }; if ns_prefix.is_some() && !matches!(*input.next_including_whitespace()?, Token::Delim('|')) { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } if let Some(prefix) = ns_prefix { let ns = match get_namespace_for_prefix(&prefix, context) { Some(ns) => ns, None => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), }; Ok((prefix, ns)) } else { Ok((Prefix::default(), Namespace::default())) } } 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> { // Syntax is `[namespace? '|']? ident [',' fallback]?` let namespace = input .try_parse(|input| parse_namespace(context, input)) .ok(); let namespace_is_some = namespace.is_some(); let (namespace_prefix, namespace_url) = namespace.unwrap_or_default(); // If there is a namespace, ensure no whitespace following '|' let attribute = Atom::from(if namespace_is_some { let location = input.current_source_location(); match *input.next_including_whitespace()? { Token::Ident(ref ident) => ident.as_ref(), ref t => return Err(location.new_unexpected_token_error(t.clone())), } } else { input.expect_ident()?.as_ref() }); // Fallback will always be a string value for now as we do not support // attr() types yet. let fallback = input .try_parse(|input| -> Result> { input.expect_comma()?; Ok(input.expect_string()?.as_ref().into()) }) .unwrap_or_default(); Ok(Attr { namespace_prefix, namespace_url, attribute, fallback, }) } } impl ToCss for Attr { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: Write, { dest.write_str("attr(")?; if !self.namespace_prefix.is_empty() { serialize_atom_identifier(&self.namespace_prefix, dest)?; dest.write_char('|')?; } serialize_atom_identifier(&self.attribute, dest)?; if !self.fallback.is_empty() { dest.write_str(", ")?; self.fallback.to_css(dest)?; } dest.write_char(')') } }