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