Skip to main content

style/values/specified/
mod.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Specified values.
6//!
7//! TODO(emilio): Enhance docs.
8
9use super::computed::{Context, ToComputedValue};
10use super::generics::grid::ImplicitGridTracks as GenericImplicitGridTracks;
11use super::generics::grid::{GridLine as GenericGridLine, TrackBreadth as GenericTrackBreadth};
12use super::generics::grid::{TrackList as GenericTrackList, TrackSize as GenericTrackSize};
13use super::generics::{self, NonNegative};
14use super::CSSFloat;
15use crate::context::QuirksMode;
16use crate::derives::*;
17use crate::parser::{Parse, ParserContext};
18use crate::values::specified::number::parse_number_with_clamping_mode;
19use crate::values::{computed, serialize_atom_identifier, AtomString};
20use crate::{Atom, Namespace, Prefix};
21use cssparser::{Parser, Token};
22use rustc_hash::FxHashMap;
23use std::fmt::{self, Write};
24use style_traits::values::specified::AllowedNumericType;
25use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
26
27pub use self::align::{ContentDistribution, ItemPlacement, JustifyItems, SelfAlignment};
28pub use self::angle::{AllowUnitlessZeroAngle, Angle, NoCalcAngle};
29pub use self::animation::{
30    AnimationComposition, AnimationDirection, AnimationDuration, AnimationFillMode,
31    AnimationIterationCount, AnimationName, AnimationPlayState, AnimationRangeEnd,
32    AnimationRangeStart, AnimationTimeline, ScrollAxis, TimelineName, TransitionBehavior,
33    TransitionProperty, ViewTimelineInset, ViewTransitionClass, ViewTransitionName,
34};
35pub use self::background::{BackgroundRepeat, BackgroundSize};
36pub use self::basic_shape::FillRule;
37pub use self::border::{
38    BorderCornerRadius, BorderImageRepeat, BorderImageSideWidth, BorderImageSlice,
39    BorderImageWidth, BorderRadius, BorderSideOffset, BorderSideWidth, BorderSpacing, BorderStyle,
40    LineWidth,
41};
42pub use self::box_::{
43    AlignmentBaseline, Appearance, BaselineShift, BaselineSource, BreakBetween, BreakWithin, Clear,
44    Contain, ContainIntrinsicSize, ContainerName, ContainerType, ContentVisibility, Display,
45    DominantBaseline, Float, LineClamp, Overflow, OverflowAnchor, OverflowClipMargin,
46    OverscrollBehavior, Perspective, PositionProperty, Resize, ScrollSnapAlign, ScrollSnapAxis,
47    ScrollSnapStop, ScrollSnapStrictness, ScrollSnapType, ScrollbarGutter, TouchAction, WillChange,
48    WillChangeBits, WritingModeProperty, Zoom,
49};
50pub use self::calc::{CalcLengthPercentage, CalcNumeric};
51pub use self::color::{
52    Color, ColorOrAuto, ColorPropertyValue, ColorScheme, ForcedColorAdjust, PrintColorAdjust,
53};
54pub use self::column::ColumnCount;
55pub use self::corner_shape::{CornerShape, CornerShapeRect, SuperellipseArg};
56pub use self::counters::{Content, ContentItem, CounterIncrement, CounterReset, CounterSet};
57pub use self::easing::TimingFunction;
58pub use self::effects::{BoxShadow, Filter, SimpleShadow};
59pub use self::flex::FlexBasis;
60pub use self::font::{FontFamily, FontLanguageOverride, FontPalette, FontStyle};
61pub use self::font::{FontFeatureSettings, FontVariantLigatures, FontVariantNumeric};
62pub use self::font::{
63    FontSize, FontSizeAdjust, FontSizeAdjustFactor, FontSizeKeyword, FontStretch, FontSynthesis,
64    FontSynthesisStyle,
65};
66pub use self::font::{FontVariantAlternates, FontWeight};
67pub use self::font::{FontVariantEastAsian, FontVariationSettings, LineHeight};
68pub use self::font::{MathDepth, MozScriptMinSize, MozScriptSizeMultiplier, XLang, XTextScale};
69pub use self::image::{EndingShape as GradientEndingShape, Gradient, Image, ImageRendering};
70pub use self::length::{Length, LengthOrNumber, LengthUnit, NonNegativeLengthOrNumber};
71pub use self::length::{LengthOrAuto, LengthPercentage, LengthPercentageOrAuto};
72pub use self::length::{Margin, MaxSize, Size};
73pub use self::length::{NoCalcLength, ViewportVariant};
74pub use self::length::{
75    NonNegativeLength, NonNegativeLengthPercentage, NonNegativeLengthPercentageOrAuto,
76};
77pub use self::list::ListStyleType;
78pub use self::list::Quotes;
79pub use self::motion::{OffsetPath, OffsetPosition, OffsetRotate};
80pub use self::number::{
81    GreaterThanOrEqualToOneNumber, Integer, NoCalcNumber, NonNegativeInteger, NonNegativeNumber,
82    Number, PositiveInteger,
83};
84pub use self::outline::OutlineStyle;
85pub use self::page::{PageName, PageOrientation, PageSize, PageSizeOrientation, PaperSize};
86pub use self::percentage::{NoCalcPercentage, NonNegativePercentage, Percentage};
87pub use self::position::{
88    AnchorFunction, AnchorName, AnchorNameIdent, AspectRatio, GridAutoFlow, GridTemplateAreas,
89    Inset, MasonryAutoFlow, MasonryItemOrder, MasonryPlacement, Position, PositionAnchor,
90    PositionAnchorKeyword, PositionArea, PositionAreaKeyword, PositionComponent, PositionOrAuto,
91    PositionTryFallbacks, PositionTryOrder, PositionVisibility, ScopedName, ZIndex,
92};
93pub use self::ratio::Ratio;
94pub use self::rect::NonNegativeLengthOrNumberRect;
95pub use self::resolution::{NoCalcResolution, Resolution};
96pub use self::svg::{DProperty, MozContextProperties};
97pub use self::svg::{SVGLength, SVGOpacity, SVGPaint};
98pub use self::svg::{SVGPaintOrder, SVGStrokeDashArray, SVGWidth, VectorEffect};
99pub use self::svg_path::SVGPathData;
100pub use self::text::RubyPosition;
101pub use self::text::{HyphenateCharacter, HyphenateLimitChars};
102pub use self::text::{InitialLetter, LetterSpacing, LineBreak, TextAlign, TextIndent};
103pub use self::text::{OverflowWrap, TextEmphasisPosition, TextEmphasisStyle, WordBreak};
104pub use self::text::{TextAlignKeyword, TextDecorationLine, TextOverflow, WordSpacing};
105pub use self::text::{TextAlignLast, TextAutospace, TextUnderlinePosition};
106pub use self::text::{TextBoxEdge, TextBoxTrim};
107pub use self::text::{
108    TextDecorationInset, TextDecorationLength, TextDecorationSkipInk, TextJustify, TextTransform,
109};
110pub use self::time::{NoCalcTime, Time};
111pub use self::transform::{Rotate, Scale, Transform};
112pub use self::transform::{TransformBox, TransformOrigin, TransformStyle, Translate};
113#[cfg(feature = "gecko")]
114pub use self::ui::CursorImage;
115pub use self::ui::{
116    BoolInteger, Cursor, Inert, MozTheme, PointerEvents, ScrollbarColor, UserFocus, UserSelect,
117};
118pub use super::generics::grid::GridTemplateComponent as GenericGridTemplateComponent;
119
120pub mod align;
121pub mod angle;
122pub mod animation;
123pub mod background;
124pub mod basic_shape;
125pub mod border;
126#[path = "box.rs"]
127pub mod box_;
128pub mod calc;
129pub mod color;
130pub mod column;
131pub mod corner_shape;
132pub mod counters;
133pub mod easing;
134pub mod effects;
135pub mod flex;
136pub mod font;
137pub mod grid;
138pub mod image;
139pub mod intersection_observer;
140pub mod length;
141pub mod list;
142pub mod motion;
143pub mod number;
144pub mod outline;
145pub mod page;
146pub mod percentage;
147pub mod position;
148pub mod ratio;
149pub mod rect;
150pub mod resolution;
151pub mod source_size_list;
152pub mod svg;
153pub mod svg_path;
154pub mod table;
155pub mod text;
156pub mod time;
157pub mod transform;
158pub mod ui;
159pub mod url;
160
161/// <angle> | <percentage>
162/// https://drafts.csswg.org/css-values/#typedef-angle-percentage
163#[allow(missing_docs)]
164#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
165pub enum AngleOrPercentage {
166    Percentage(Percentage),
167    Angle(Angle),
168}
169
170impl AngleOrPercentage {
171    fn parse_internal<'i, 't>(
172        context: &ParserContext,
173        input: &mut Parser<'i, 't>,
174        allow_unitless_zero: AllowUnitlessZeroAngle,
175    ) -> Result<Self, ParseError<'i>> {
176        if let Ok(per) = input.try_parse(|i| Percentage::parse(context, i)) {
177            return Ok(AngleOrPercentage::Percentage(per));
178        }
179
180        Angle::parse_internal(context, input, allow_unitless_zero).map(AngleOrPercentage::Angle)
181    }
182
183    /// Allow unitless angles, used for conic-gradients as specified by the spec.
184    /// https://drafts.csswg.org/css-images-4/#valdef-conic-gradient-angle
185    pub fn parse_with_unitless<'i, 't>(
186        context: &ParserContext,
187        input: &mut Parser<'i, 't>,
188    ) -> Result<Self, ParseError<'i>> {
189        AngleOrPercentage::parse_internal(context, input, AllowUnitlessZeroAngle::Yes)
190    }
191}
192
193impl Parse for AngleOrPercentage {
194    fn parse<'i, 't>(
195        context: &ParserContext,
196        input: &mut Parser<'i, 't>,
197    ) -> Result<Self, ParseError<'i>> {
198        AngleOrPercentage::parse_internal(context, input, AllowUnitlessZeroAngle::No)
199    }
200}
201
202/// <number> | <percentage>
203///
204/// Accepts only non-negative numbers.
205///
206/// TODO(Bug 2040559) - Convert this into a NumericUnion, instead of an enum over
207/// Number and Percentage. Both types are also NumericUnions of unitless floats.
208#[allow(missing_docs)]
209#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, ToTyped)]
210pub enum NumberOrPercentage {
211    Percentage(Percentage),
212    Number(Number),
213}
214
215impl NumberOrPercentage {
216    fn parse_with_clamping_mode<'i, 't>(
217        context: &ParserContext,
218        input: &mut Parser<'i, 't>,
219        type_: AllowedNumericType,
220    ) -> Result<Self, ParseError<'i>> {
221        if let Ok(per) =
222            input.try_parse(|i| Percentage::parse_with_clamping_mode(context, i, type_))
223        {
224            return Ok(NumberOrPercentage::Percentage(per));
225        }
226
227        parse_number_with_clamping_mode(context, input, type_).map(NumberOrPercentage::Number)
228    }
229
230    /// Parse a non-negative number or percentage.
231    pub fn parse_non_negative<'i, 't>(
232        context: &ParserContext,
233        input: &mut Parser<'i, 't>,
234    ) -> Result<Self, ParseError<'i>> {
235        Self::parse_with_clamping_mode(context, input, AllowedNumericType::NonNegative)
236    }
237
238    /// Convert the number or the percentage to a number.
239    pub fn to_percentage(self) -> Option<Percentage> {
240        match self {
241            Self::Percentage(p) => Some(p),
242            Self::Number(n) => n.to_percentage(),
243        }
244    }
245
246    /// Convert the number or the percentage to a number.
247    pub fn to_number(&self) -> Option<Number> {
248        match self {
249            Self::Percentage(p) => p.to_number(),
250            Self::Number(n) => Some(n.clone()),
251        }
252    }
253
254    /// Gets a reference to the underlying percentage, or None if this is a number
255    pub fn as_percentage(&self) -> Option<&Percentage> {
256        match self {
257            NumberOrPercentage::Percentage(percentage) => Some(percentage),
258            _ => None,
259        }
260    }
261
262    /// If this is a non-calc percentage, replaces it with the equivalent
263    /// number; otherwise, returns the original value.
264    pub fn into_simplified_number(self) -> NumberOrPercentage {
265        match self.as_percentage().and_then(|p| p.get()) {
266            Some(p) => NumberOrPercentage::Number(Number::new(p)),
267            None => self,
268        }
269    }
270
271    /// Attempts to resolve this number or percentage to a computed value.
272    pub fn to_computed_value_without_context(&self) -> Result<computed::NumberOrPercentage, ()> {
273        Ok(match self {
274            NumberOrPercentage::Percentage(percentage) => computed::NumberOrPercentage::Percentage(
275                computed::Percentage(percentage.resolve().ok_or(())?),
276            ),
277            NumberOrPercentage::Number(number) => {
278                computed::NumberOrPercentage::Number(number.resolve().ok_or(())?)
279            },
280        })
281    }
282}
283
284impl Parse for NumberOrPercentage {
285    fn parse<'i, 't>(
286        context: &ParserContext,
287        input: &mut Parser<'i, 't>,
288    ) -> Result<Self, ParseError<'i>> {
289        Self::parse_with_clamping_mode(context, input, AllowedNumericType::All)
290    }
291}
292
293/// A non-negative <number> | <percentage>.
294pub type NonNegativeNumberOrPercentage = NonNegative<NumberOrPercentage>;
295
296impl NonNegativeNumberOrPercentage {
297    /// Returns the `100%` value.
298    #[inline]
299    pub fn hundred_percent() -> Self {
300        NonNegative(NumberOrPercentage::Percentage(Percentage::hundred()))
301    }
302
303    /// Return a particular number.
304    #[inline]
305    pub fn new_number(n: f32) -> Self {
306        NonNegative(NumberOrPercentage::Number(Number::new(n)))
307    }
308}
309
310impl Parse for NonNegativeNumberOrPercentage {
311    fn parse<'i, 't>(
312        context: &ParserContext,
313        input: &mut Parser<'i, 't>,
314    ) -> Result<Self, ParseError<'i>> {
315        Ok(NonNegative(NumberOrPercentage::parse_non_negative(
316            context, input,
317        )?))
318    }
319}
320
321/// A specified CSS `opacity`
322#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, ToTyped)]
323pub struct Opacity(NumberOrPercentage);
324
325impl Parse for Opacity {
326    /// Opacity accepts <number> | <percentage>, so we parse it as NumberOrPercentage,
327    /// and then convert into an Number if it's a non-calc Percentage.
328    /// https://drafts.csswg.org/css-color-4/#serializing-opacity-values
329    fn parse<'i, 't>(
330        context: &ParserContext,
331        input: &mut Parser<'i, 't>,
332    ) -> Result<Self, ParseError<'i>> {
333        Ok(Opacity(
334            NumberOrPercentage::parse(context, input)?.into_simplified_number(),
335        ))
336    }
337}
338
339impl ToComputedValue for Opacity {
340    type ComputedValue = CSSFloat;
341
342    #[inline]
343    fn to_computed_value(&self, context: &Context) -> CSSFloat {
344        let value = self.0.to_computed_value(context).value();
345        if context.for_animation {
346            // Type <number> and <percentage> should be able to interpolate
347            // out-of-range opacity values which benefits additive animation
348            value
349        } else {
350            value.min(1.0).max(0.0)
351        }
352    }
353
354    #[inline]
355    fn from_computed_value(computed: &CSSFloat) -> Self {
356        Opacity(NumberOrPercentage::Number(Number::from_computed_value(
357            computed,
358        )))
359    }
360}
361
362/// The specified value of a grid `<track-breadth>`
363pub type TrackBreadth = GenericTrackBreadth<LengthPercentage>;
364
365/// The specified value of a grid `<track-size>`
366pub type TrackSize = GenericTrackSize<LengthPercentage>;
367
368/// The specified value of a grid `<track-size>+`
369pub type ImplicitGridTracks = GenericImplicitGridTracks<TrackSize>;
370
371/// The specified value of a grid `<track-list>`
372/// (could also be `<auto-track-list>` or `<explicit-track-list>`)
373pub type TrackList = GenericTrackList<LengthPercentage, Integer>;
374
375/// The specified value of a `<grid-line>`.
376pub type GridLine = GenericGridLine<Integer>;
377
378/// `<grid-template-rows> | <grid-template-columns>`
379pub type GridTemplateComponent = GenericGridTemplateComponent<LengthPercentage, Integer>;
380
381/// rect(...)
382pub type ClipRect = generics::GenericClipRect<LengthOrAuto>;
383
384impl Parse for ClipRect {
385    fn parse<'i, 't>(
386        context: &ParserContext,
387        input: &mut Parser<'i, 't>,
388    ) -> Result<Self, ParseError<'i>> {
389        Self::parse_quirky(context, input, AllowQuirks::No)
390    }
391}
392
393impl ClipRect {
394    /// Parses a rect(<top>, <left>, <bottom>, <right>), allowing quirks.
395    fn parse_quirky<'i, 't>(
396        context: &ParserContext,
397        input: &mut Parser<'i, 't>,
398        allow_quirks: AllowQuirks,
399    ) -> Result<Self, ParseError<'i>> {
400        input.expect_function_matching("rect")?;
401
402        fn parse_argument<'i, 't>(
403            context: &ParserContext,
404            input: &mut Parser<'i, 't>,
405            allow_quirks: AllowQuirks,
406        ) -> Result<LengthOrAuto, ParseError<'i>> {
407            LengthOrAuto::parse_quirky(context, input, allow_quirks)
408        }
409
410        input.parse_nested_block(|input| {
411            let top = parse_argument(context, input, allow_quirks)?;
412            let right;
413            let bottom;
414            let left;
415
416            if input.try_parse(|input| input.expect_comma()).is_ok() {
417                right = parse_argument(context, input, allow_quirks)?;
418                input.expect_comma()?;
419                bottom = parse_argument(context, input, allow_quirks)?;
420                input.expect_comma()?;
421                left = parse_argument(context, input, allow_quirks)?;
422            } else {
423                right = parse_argument(context, input, allow_quirks)?;
424                bottom = parse_argument(context, input, allow_quirks)?;
425                left = parse_argument(context, input, allow_quirks)?;
426            }
427
428            Ok(ClipRect {
429                top,
430                right,
431                bottom,
432                left,
433            })
434        })
435    }
436}
437
438/// rect(...) | auto
439pub type ClipRectOrAuto = generics::GenericClipRectOrAuto<ClipRect>;
440
441impl ClipRectOrAuto {
442    /// Parses a ClipRect or Auto, allowing quirks.
443    pub fn parse_quirky<'i, 't>(
444        context: &ParserContext,
445        input: &mut Parser<'i, 't>,
446        allow_quirks: AllowQuirks,
447    ) -> Result<Self, ParseError<'i>> {
448        if let Ok(v) = input.try_parse(|i| ClipRect::parse_quirky(context, i, allow_quirks)) {
449            return Ok(generics::GenericClipRectOrAuto::Rect(v));
450        }
451        input.expect_ident_matching("auto")?;
452        Ok(generics::GenericClipRectOrAuto::Auto)
453    }
454}
455
456/// Whether quirks are allowed in this context.
457#[derive(Clone, Copy, PartialEq)]
458pub enum AllowQuirks {
459    /// Quirks are not allowed.
460    No,
461    /// Quirks are allowed, in quirks mode.
462    Yes,
463    /// Quirks are always allowed, used for SVG lengths.
464    Always,
465}
466
467impl AllowQuirks {
468    /// Returns `true` if quirks are allowed in this context.
469    pub fn allowed(self, quirks_mode: QuirksMode) -> bool {
470        match self {
471            AllowQuirks::Always => true,
472            AllowQuirks::No => false,
473            AllowQuirks::Yes => quirks_mode == QuirksMode::Quirks,
474        }
475    }
476}
477
478#[derive(Clone, Debug, PartialEq, MallocSizeOf, ToShmem)]
479/// A namespace wrapper to distinguish between valid variants
480pub enum ParsedNamespace {
481    /// Unregistered namespace
482    Unknown,
483    /// Registered namespace
484    Known(Namespace),
485}
486
487impl ParsedNamespace {
488    /// Parse a namespace prefix and resolve it to the correct
489    /// namespace URI.
490    pub fn parse<'i, 't>(
491        namespaces: &FxHashMap<Prefix, Namespace>,
492        input: &mut Parser<'i, 't>,
493    ) -> Result<Self, ParseError<'i>> {
494        // We don't need to keep the prefix because different
495        // prefixes can resolve to the same id. Additionally,
496        // we also don't need it for serialization as substitution
497        // functions serialize from the direct css declaration.
498        parse_namespace(namespaces, input, /*allow_non_registered*/ true)
499            .map(|(_prefix, namespace)| namespace)
500    }
501}
502
503impl Default for ParsedNamespace {
504    fn default() -> Self {
505        Self::Known(Namespace::default())
506    }
507}
508
509/// An attr(...) rule
510///
511/// `[namespace? `|`]? ident`
512#[derive(
513    Clone,
514    Debug,
515    Eq,
516    MallocSizeOf,
517    PartialEq,
518    SpecifiedValueInfo,
519    ToComputedValue,
520    ToResolvedValue,
521    ToShmem,
522)]
523#[css(function)]
524#[repr(C)]
525pub struct Attr {
526    /// Optional namespace prefix.
527    pub namespace_prefix: Prefix,
528    /// Optional namespace URL.
529    pub namespace_url: Namespace,
530    /// Attribute name
531    pub attribute: Atom,
532    /// Fallback value
533    pub fallback: AtomString,
534}
535
536impl Parse for Attr {
537    fn parse<'i, 't>(
538        context: &ParserContext,
539        input: &mut Parser<'i, 't>,
540    ) -> Result<Attr, ParseError<'i>> {
541        input.expect_function_matching("attr")?;
542        input.parse_nested_block(|i| Attr::parse_function(context, i))
543    }
544}
545
546/// Try to parse a namespace and return it if parsed, or none if there was not one present
547pub fn parse_namespace<'i, 't>(
548    namespaces: &FxHashMap<Prefix, Namespace>,
549    input: &mut Parser<'i, 't>,
550    // TODO: Once general attr is enabled, we should remove this flag
551    allow_non_registered: bool,
552) -> Result<(Prefix, ParsedNamespace), ParseError<'i>> {
553    let ns_prefix = match input.next()? {
554        Token::Ident(ref prefix) => Some(Prefix::from(prefix.as_ref())),
555        Token::Delim('|') => None,
556        _ => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
557    };
558
559    if ns_prefix.is_some() && !matches!(*input.next_including_whitespace()?, Token::Delim('|')) {
560        return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
561    }
562
563    if let Some(prefix) = ns_prefix {
564        let ns = match namespaces.get(&prefix).cloned() {
565            Some(ns) => ParsedNamespace::Known(ns),
566            None => {
567                if allow_non_registered {
568                    ParsedNamespace::Unknown
569                } else {
570                    return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
571                }
572            },
573        };
574        Ok((prefix, ns))
575    } else {
576        Ok((Prefix::default(), ParsedNamespace::default()))
577    }
578}
579
580impl Attr {
581    /// Parse contents of attr() assuming we have already parsed `attr` and are
582    /// within a parse_nested_block()
583    pub fn parse_function<'i, 't>(
584        context: &ParserContext,
585        input: &mut Parser<'i, 't>,
586    ) -> Result<Attr, ParseError<'i>> {
587        // Syntax is `[namespace? '|']? ident [',' fallback]?`
588        let namespace = input
589            .try_parse(|input| {
590                parse_namespace(
591                    &context.namespaces.prefixes,
592                    input,
593                    /*allow_non_registered*/ false,
594                )
595            })
596            .ok();
597        let namespace_is_some = namespace.is_some();
598        let (namespace_prefix, namespace_url) = namespace.unwrap_or_default();
599        let ParsedNamespace::Known(namespace_url) = namespace_url else {
600            unreachable!("Non-registered url not allowed (see parse namespace flag).")
601        };
602
603        // If there is a namespace, ensure no whitespace following '|'
604        let attribute = Atom::from(if namespace_is_some {
605            let location = input.current_source_location();
606            match *input.next_including_whitespace()? {
607                Token::Ident(ref ident) => ident.as_ref(),
608                ref t => return Err(location.new_unexpected_token_error(t.clone())),
609            }
610        } else {
611            input.expect_ident()?.as_ref()
612        });
613
614        // Fallback will always be a string value for now as we do not support
615        // attr() types yet.
616        let fallback = input
617            .try_parse(|input| -> Result<AtomString, ParseError<'i>> {
618                input.expect_comma()?;
619                Ok(input.expect_string()?.as_ref().into())
620            })
621            .unwrap_or_default();
622
623        Ok(Attr {
624            namespace_prefix,
625            namespace_url,
626            attribute,
627            fallback,
628        })
629    }
630}
631
632impl ToCss for Attr {
633    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
634    where
635        W: Write,
636    {
637        dest.write_str("attr(")?;
638        if !self.namespace_prefix.is_empty() {
639            serialize_atom_identifier(&self.namespace_prefix, dest)?;
640            dest.write_char('|')?;
641        }
642        serialize_atom_identifier(&self.attribute, dest)?;
643
644        if !self.fallback.is_empty() {
645            dest.write_str(", ")?;
646            self.fallback.to_css(dest)?;
647        }
648
649        dest.write_char(')')
650    }
651}