style/values/specified/
text.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 types for text properties.
6
7use crate::derives::*;
8use crate::parser::{Parse, ParserContext};
9use crate::properties::longhands::writing_mode::computed_value::T as SpecifiedWritingMode;
10use crate::values::computed;
11use crate::values::computed::text::TextEmphasisStyle as ComputedTextEmphasisStyle;
12use crate::values::computed::{Context, ToComputedValue};
13use crate::values::generics::text::{
14    GenericHyphenateLimitChars, GenericInitialLetter, GenericTextDecorationInset,
15    GenericTextDecorationLength, GenericTextIndent,
16};
17use crate::values::generics::NumberOrAuto;
18use crate::values::specified::length::{Length, LengthPercentage};
19use crate::values::specified::{AllowQuirks, Integer, Number};
20use crate::Zero;
21use cssparser::Parser;
22use icu_segmenter::GraphemeClusterSegmenter;
23use std::fmt::{self, Write};
24use style_traits::values::SequenceWriter;
25use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
26use style_traits::{KeywordsCollectFn, SpecifiedValueInfo};
27
28/// A specified type for the `initial-letter` property.
29pub type InitialLetter = GenericInitialLetter<Number, Integer>;
30
31/// A spacing value used by either the `letter-spacing` or `word-spacing` properties.
32#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, ToTyped)]
33pub enum Spacing {
34    /// `normal`
35    Normal,
36    /// `<value>`
37    Value(LengthPercentage),
38}
39
40impl Parse for Spacing {
41    fn parse<'i, 't>(
42        context: &ParserContext,
43        input: &mut Parser<'i, 't>,
44    ) -> Result<Self, ParseError<'i>> {
45        if input
46            .try_parse(|i| i.expect_ident_matching("normal"))
47            .is_ok()
48        {
49            return Ok(Spacing::Normal);
50        }
51        LengthPercentage::parse_quirky(context, input, AllowQuirks::Yes).map(Spacing::Value)
52    }
53}
54
55/// A specified value for the `letter-spacing` property.
56#[derive(
57    Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, ToTyped,
58)]
59#[typed_value(derive_fields)]
60pub struct LetterSpacing(pub Spacing);
61
62impl ToComputedValue for LetterSpacing {
63    type ComputedValue = computed::LetterSpacing;
64
65    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
66        use computed::text::GenericLetterSpacing;
67        match self.0 {
68            Spacing::Normal => GenericLetterSpacing(computed::LengthPercentage::zero()),
69            Spacing::Value(ref v) => GenericLetterSpacing(v.to_computed_value(context)),
70        }
71    }
72
73    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
74        if computed.0.is_zero() {
75            return LetterSpacing(Spacing::Normal);
76        }
77        LetterSpacing(Spacing::Value(ToComputedValue::from_computed_value(
78            &computed.0,
79        )))
80    }
81}
82
83/// A specified value for the `word-spacing` property.
84#[derive(
85    Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, ToTyped,
86)]
87pub struct WordSpacing(pub Spacing);
88
89impl ToComputedValue for WordSpacing {
90    type ComputedValue = computed::WordSpacing;
91
92    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
93        match self.0 {
94            Spacing::Normal => computed::LengthPercentage::zero(),
95            Spacing::Value(ref v) => v.to_computed_value(context),
96        }
97    }
98
99    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
100        WordSpacing(Spacing::Value(ToComputedValue::from_computed_value(
101            computed,
102        )))
103    }
104}
105
106/// A value for the `hyphenate-character` property.
107#[derive(
108    Clone,
109    Debug,
110    MallocSizeOf,
111    Parse,
112    PartialEq,
113    SpecifiedValueInfo,
114    ToComputedValue,
115    ToCss,
116    ToResolvedValue,
117    ToShmem,
118    ToTyped,
119)]
120#[repr(C, u8)]
121pub enum HyphenateCharacter {
122    /// `auto`
123    Auto,
124    /// `<string>`
125    String(crate::OwnedStr),
126}
127
128/// A value for the `hyphenate-limit-chars` property.
129pub type HyphenateLimitChars = GenericHyphenateLimitChars<Integer>;
130
131impl Parse for HyphenateLimitChars {
132    fn parse<'i, 't>(
133        context: &ParserContext,
134        input: &mut Parser<'i, 't>,
135    ) -> Result<Self, ParseError<'i>> {
136        type IntegerOrAuto = NumberOrAuto<Integer>;
137
138        let total_word_length = IntegerOrAuto::parse(context, input)?;
139        let pre_hyphen_length = input
140            .try_parse(|i| IntegerOrAuto::parse(context, i))
141            .unwrap_or(IntegerOrAuto::Auto);
142        let post_hyphen_length = input
143            .try_parse(|i| IntegerOrAuto::parse(context, i))
144            .unwrap_or(pre_hyphen_length);
145        Ok(Self {
146            total_word_length,
147            pre_hyphen_length,
148            post_hyphen_length,
149        })
150    }
151}
152
153impl Parse for InitialLetter {
154    fn parse<'i, 't>(
155        context: &ParserContext,
156        input: &mut Parser<'i, 't>,
157    ) -> Result<Self, ParseError<'i>> {
158        if input
159            .try_parse(|i| i.expect_ident_matching("normal"))
160            .is_ok()
161        {
162            return Ok(Self::normal());
163        }
164        let size = Number::parse_at_least_one(context, input)?;
165        let sink = input
166            .try_parse(|i| Integer::parse_positive(context, i))
167            .unwrap_or_else(|_| crate::Zero::zero());
168        Ok(Self { size, sink })
169    }
170}
171
172/// A generic value for the `text-overflow` property.
173#[derive(
174    Clone,
175    Debug,
176    Eq,
177    MallocSizeOf,
178    PartialEq,
179    Parse,
180    SpecifiedValueInfo,
181    ToComputedValue,
182    ToCss,
183    ToResolvedValue,
184    ToShmem,
185)]
186#[repr(C, u8)]
187pub enum TextOverflowSide {
188    /// Clip inline content.
189    Clip,
190    /// Render ellipsis to represent clipped inline content.
191    Ellipsis,
192    /// Render a given string to represent clipped inline content.
193    String(crate::values::AtomString),
194}
195
196#[derive(
197    Clone,
198    Debug,
199    Eq,
200    MallocSizeOf,
201    PartialEq,
202    SpecifiedValueInfo,
203    ToComputedValue,
204    ToResolvedValue,
205    ToShmem,
206    ToTyped,
207)]
208#[repr(C)]
209/// text-overflow.
210/// When the specified value only has one side, that's the "second"
211/// side, and the sides are logical, so "second" means "end".  The
212/// start side is Clip in that case.
213///
214/// When the specified value has two sides, those are our "first"
215/// and "second" sides, and they are physical sides ("left" and
216/// "right").
217pub struct TextOverflow {
218    /// First side
219    pub first: TextOverflowSide,
220    /// Second side
221    pub second: TextOverflowSide,
222    /// True if the specified value only has one side.
223    pub sides_are_logical: bool,
224}
225
226impl Parse for TextOverflow {
227    fn parse<'i, 't>(
228        context: &ParserContext,
229        input: &mut Parser<'i, 't>,
230    ) -> Result<TextOverflow, ParseError<'i>> {
231        let first = TextOverflowSide::parse(context, input)?;
232        Ok(
233            if let Ok(second) = input.try_parse(|input| TextOverflowSide::parse(context, input)) {
234                Self {
235                    first,
236                    second,
237                    sides_are_logical: false,
238                }
239            } else {
240                Self {
241                    first: TextOverflowSide::Clip,
242                    second: first,
243                    sides_are_logical: true,
244                }
245            },
246        )
247    }
248}
249
250impl TextOverflow {
251    /// Returns the initial `text-overflow` value
252    pub fn get_initial_value() -> TextOverflow {
253        TextOverflow {
254            first: TextOverflowSide::Clip,
255            second: TextOverflowSide::Clip,
256            sides_are_logical: true,
257        }
258    }
259}
260
261impl ToCss for TextOverflow {
262    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
263    where
264        W: Write,
265    {
266        if self.sides_are_logical {
267            debug_assert_eq!(self.first, TextOverflowSide::Clip);
268            self.second.to_css(dest)?;
269        } else {
270            self.first.to_css(dest)?;
271            dest.write_char(' ')?;
272            self.second.to_css(dest)?;
273        }
274        Ok(())
275    }
276}
277
278#[derive(
279    Clone,
280    Copy,
281    Debug,
282    Eq,
283    MallocSizeOf,
284    PartialEq,
285    Parse,
286    Serialize,
287    SpecifiedValueInfo,
288    ToCss,
289    ToComputedValue,
290    ToResolvedValue,
291    ToShmem,
292    ToTyped,
293)]
294#[cfg_attr(
295    feature = "gecko",
296    css(bitflags(
297        single = "none,spelling-error,grammar-error",
298        mixed = "underline,overline,line-through,blink",
299    ))
300)]
301#[cfg_attr(
302    not(feature = "gecko"),
303    css(bitflags(single = "none", mixed = "underline,overline,line-through,blink",))
304)]
305#[repr(C)]
306/// Specified keyword values for the text-decoration-line property.
307pub struct TextDecorationLine(u8);
308bitflags! {
309    impl TextDecorationLine: u8 {
310        /// No text decoration line is specified.
311        const NONE = 0;
312        /// underline
313        const UNDERLINE = 1 << 0;
314        /// overline
315        const OVERLINE = 1 << 1;
316        /// line-through
317        const LINE_THROUGH = 1 << 2;
318        /// blink
319        const BLINK = 1 << 3;
320        /// spelling-error
321        const SPELLING_ERROR = 1 << 4;
322        /// grammar-error
323        const GRAMMAR_ERROR = 1 << 5;
324        /// Only set by presentation attributes
325        ///
326        /// Setting this will mean that text-decorations use the color
327        /// specified by `color` in quirks mode.
328        ///
329        /// For example, this gives <a href=foo><font color="red">text</font></a>
330        /// a red text decoration
331        #[cfg(feature = "gecko")]
332        const COLOR_OVERRIDE = 1 << 7;
333    }
334}
335
336impl Default for TextDecorationLine {
337    fn default() -> Self {
338        TextDecorationLine::NONE
339    }
340}
341
342impl TextDecorationLine {
343    #[inline]
344    /// Returns the initial value of text-decoration-line
345    pub fn none() -> Self {
346        TextDecorationLine::NONE
347    }
348}
349
350#[derive(
351    Clone,
352    Copy,
353    Debug,
354    Eq,
355    MallocSizeOf,
356    PartialEq,
357    SpecifiedValueInfo,
358    ToComputedValue,
359    ToCss,
360    ToResolvedValue,
361    ToShmem,
362)]
363#[repr(C)]
364/// Specified keyword values for case transforms in the text-transform property. (These are exclusive.)
365pub enum TextTransformCase {
366    /// No case transform.
367    None,
368    /// All uppercase.
369    Uppercase,
370    /// All lowercase.
371    Lowercase,
372    /// Capitalize each word.
373    Capitalize,
374    /// Automatic italicization of math variables.
375    #[cfg(feature = "gecko")]
376    MathAuto,
377}
378
379#[derive(
380    Clone,
381    Copy,
382    Debug,
383    Eq,
384    MallocSizeOf,
385    PartialEq,
386    Parse,
387    Serialize,
388    SpecifiedValueInfo,
389    ToCss,
390    ToComputedValue,
391    ToResolvedValue,
392    ToShmem,
393    ToTyped,
394)]
395#[cfg_attr(
396    feature = "gecko",
397    css(bitflags(
398        single = "none,math-auto",
399        mixed = "uppercase,lowercase,capitalize,full-width,full-size-kana",
400        validate_mixed = "Self::validate_mixed_flags",
401    ))
402)]
403#[cfg_attr(
404    not(feature = "gecko"),
405    css(bitflags(
406        single = "none",
407        mixed = "uppercase,lowercase,capitalize,full-width,full-size-kana",
408        validate_mixed = "Self::validate_mixed_flags",
409    ))
410)]
411#[repr(C)]
412/// Specified value for the text-transform property.
413/// (The spec grammar gives
414/// `none | math-auto | [capitalize | uppercase | lowercase] || full-width || full-size-kana`.)
415/// https://drafts.csswg.org/css-text-4/#text-transform-property
416pub struct TextTransform(u8);
417bitflags! {
418    impl TextTransform: u8 {
419        /// none
420        const NONE = 0;
421        /// All uppercase.
422        const UPPERCASE = 1 << 0;
423        /// All lowercase.
424        const LOWERCASE = 1 << 1;
425        /// Capitalize each word.
426        const CAPITALIZE = 1 << 2;
427        /// Automatic italicization of math variables.
428        #[cfg(feature = "gecko")]
429        const MATH_AUTO = 1 << 3;
430
431        /// All the case transforms, which are exclusive with each other.
432        #[cfg(feature = "gecko")]
433        const CASE_TRANSFORMS = Self::UPPERCASE.0 | Self::LOWERCASE.0 | Self::CAPITALIZE.0 | Self::MATH_AUTO.0;
434        /// All the case transforms, which are exclusive with each other.
435        #[cfg(feature = "servo")]
436        const CASE_TRANSFORMS = Self::UPPERCASE.0 | Self::LOWERCASE.0 | Self::CAPITALIZE.0;
437
438        /// full-width
439        const FULL_WIDTH = 1 << 4;
440        /// full-size-kana
441        const FULL_SIZE_KANA = 1 << 5;
442    }
443}
444
445impl TextTransform {
446    /// Returns the initial value of text-transform
447    #[inline]
448    pub fn none() -> Self {
449        Self::NONE
450    }
451
452    /// Returns whether the value is 'none'
453    #[inline]
454    pub fn is_none(self) -> bool {
455        self == Self::NONE
456    }
457
458    fn validate_mixed_flags(&self) -> bool {
459        let case = self.intersection(Self::CASE_TRANSFORMS);
460        // Case bits are exclusive with each other.
461        case.is_empty() || case.bits().is_power_of_two()
462    }
463
464    /// Returns the corresponding TextTransformCase.
465    pub fn case(&self) -> TextTransformCase {
466        match *self & Self::CASE_TRANSFORMS {
467            Self::NONE => TextTransformCase::None,
468            Self::UPPERCASE => TextTransformCase::Uppercase,
469            Self::LOWERCASE => TextTransformCase::Lowercase,
470            Self::CAPITALIZE => TextTransformCase::Capitalize,
471            #[cfg(feature = "gecko")]
472            Self::MATH_AUTO => TextTransformCase::MathAuto,
473            _ => unreachable!("Case bits are exclusive with each other"),
474        }
475    }
476}
477
478/// Specified and computed value of text-align-last.
479#[derive(
480    Clone,
481    Copy,
482    Debug,
483    Eq,
484    FromPrimitive,
485    Hash,
486    MallocSizeOf,
487    Parse,
488    PartialEq,
489    SpecifiedValueInfo,
490    ToComputedValue,
491    ToCss,
492    ToResolvedValue,
493    ToShmem,
494    ToTyped,
495)]
496#[allow(missing_docs)]
497#[repr(u8)]
498pub enum TextAlignLast {
499    Auto,
500    Start,
501    End,
502    Left,
503    Right,
504    Center,
505    Justify,
506}
507
508/// Specified value of text-align keyword value.
509#[derive(
510    Clone,
511    Copy,
512    Debug,
513    Eq,
514    FromPrimitive,
515    Hash,
516    MallocSizeOf,
517    Parse,
518    PartialEq,
519    SpecifiedValueInfo,
520    ToComputedValue,
521    ToCss,
522    ToResolvedValue,
523    ToShmem,
524    ToTyped,
525)]
526#[allow(missing_docs)]
527#[repr(u8)]
528pub enum TextAlignKeyword {
529    Start,
530    Left,
531    Right,
532    Center,
533    Justify,
534    End,
535    #[parse(aliases = "-webkit-center")]
536    MozCenter,
537    #[parse(aliases = "-webkit-left")]
538    MozLeft,
539    #[parse(aliases = "-webkit-right")]
540    MozRight,
541}
542
543/// Specified value of text-align property.
544#[derive(
545    Clone,
546    Copy,
547    Debug,
548    Eq,
549    Hash,
550    MallocSizeOf,
551    Parse,
552    PartialEq,
553    SpecifiedValueInfo,
554    ToCss,
555    ToShmem,
556    ToTyped,
557)]
558pub enum TextAlign {
559    /// Keyword value of text-align property.
560    Keyword(TextAlignKeyword),
561    /// `match-parent` value of text-align property. It has a different handling
562    /// unlike other keywords.
563    #[cfg(feature = "gecko")]
564    MatchParent,
565    /// This is how we implement the following HTML behavior from
566    /// https://html.spec.whatwg.org/#tables-2:
567    ///
568    ///     User agents are expected to have a rule in their user agent style sheet
569    ///     that matches th elements that have a parent node whose computed value
570    ///     for the 'text-align' property is its initial value, whose declaration
571    ///     block consists of just a single declaration that sets the 'text-align'
572    ///     property to the value 'center'.
573    ///
574    /// Since selectors can't depend on the ancestor styles, we implement it with a
575    /// magic value that computes to the right thing. Since this is an
576    /// implementation detail, it shouldn't be exposed to web content.
577    #[parse(condition = "ParserContext::chrome_rules_enabled")]
578    MozCenterOrInherit,
579}
580
581impl ToComputedValue for TextAlign {
582    type ComputedValue = TextAlignKeyword;
583
584    #[inline]
585    fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue {
586        match *self {
587            TextAlign::Keyword(key) => key,
588            #[cfg(feature = "gecko")]
589            TextAlign::MatchParent => {
590                // on the root <html> element we should still respect the dir
591                // but the parent dir of that element is LTR even if it's <html dir=rtl>
592                // and will only be RTL if certain prefs have been set.
593                // In that case, the default behavior here will set it to left,
594                // but we want to set it to right -- instead set it to the default (`start`),
595                // which will do the right thing in this case (but not the general case)
596                if _context.builder.is_root_element {
597                    return TextAlignKeyword::Start;
598                }
599                let parent = _context
600                    .builder
601                    .get_parent_inherited_text()
602                    .clone_text_align();
603                let ltr = _context.builder.inherited_writing_mode().is_bidi_ltr();
604                match (parent, ltr) {
605                    (TextAlignKeyword::Start, true) => TextAlignKeyword::Left,
606                    (TextAlignKeyword::Start, false) => TextAlignKeyword::Right,
607                    (TextAlignKeyword::End, true) => TextAlignKeyword::Right,
608                    (TextAlignKeyword::End, false) => TextAlignKeyword::Left,
609                    _ => parent,
610                }
611            },
612            TextAlign::MozCenterOrInherit => {
613                let parent = _context
614                    .builder
615                    .get_parent_inherited_text()
616                    .clone_text_align();
617                if parent == TextAlignKeyword::Start {
618                    TextAlignKeyword::Center
619                } else {
620                    parent
621                }
622            },
623        }
624    }
625
626    #[inline]
627    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
628        TextAlign::Keyword(*computed)
629    }
630}
631
632fn fill_mode_is_default_and_shape_exists(
633    fill: &TextEmphasisFillMode,
634    shape: &Option<TextEmphasisShapeKeyword>,
635) -> bool {
636    shape.is_some() && fill.is_filled()
637}
638
639/// Specified value of text-emphasis-style property.
640///
641/// https://drafts.csswg.org/css-text-decor/#propdef-text-emphasis-style
642#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, ToTyped)]
643#[allow(missing_docs)]
644pub enum TextEmphasisStyle {
645    /// [ <fill> || <shape> ]
646    Keyword {
647        #[css(contextual_skip_if = "fill_mode_is_default_and_shape_exists")]
648        fill: TextEmphasisFillMode,
649        shape: Option<TextEmphasisShapeKeyword>,
650    },
651    /// `none`
652    None,
653    /// `<string>` (of which only the first grapheme cluster will be used).
654    String(crate::OwnedStr),
655}
656
657/// Fill mode for the text-emphasis-style property
658#[derive(
659    Clone,
660    Copy,
661    Debug,
662    MallocSizeOf,
663    Parse,
664    PartialEq,
665    SpecifiedValueInfo,
666    ToCss,
667    ToComputedValue,
668    ToResolvedValue,
669    ToShmem,
670)]
671#[repr(u8)]
672pub enum TextEmphasisFillMode {
673    /// `filled`
674    Filled,
675    /// `open`
676    Open,
677}
678
679impl TextEmphasisFillMode {
680    /// Whether the value is `filled`.
681    #[inline]
682    pub fn is_filled(&self) -> bool {
683        matches!(*self, TextEmphasisFillMode::Filled)
684    }
685}
686
687/// Shape keyword for the text-emphasis-style property
688#[derive(
689    Clone,
690    Copy,
691    Debug,
692    Eq,
693    MallocSizeOf,
694    Parse,
695    PartialEq,
696    SpecifiedValueInfo,
697    ToCss,
698    ToComputedValue,
699    ToResolvedValue,
700    ToShmem,
701)]
702#[repr(u8)]
703pub enum TextEmphasisShapeKeyword {
704    /// `dot`
705    Dot,
706    /// `circle`
707    Circle,
708    /// `double-circle`
709    DoubleCircle,
710    /// `triangle`
711    Triangle,
712    /// `sesame`
713    Sesame,
714}
715
716impl ToComputedValue for TextEmphasisStyle {
717    type ComputedValue = ComputedTextEmphasisStyle;
718
719    #[inline]
720    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
721        match *self {
722            TextEmphasisStyle::Keyword { fill, shape } => {
723                let shape = shape.unwrap_or_else(|| {
724                    // FIXME(emilio, bug 1572958): This should set the
725                    // rule_cache_conditions properly.
726                    //
727                    // Also should probably use WritingMode::is_vertical rather
728                    // than the computed value of the `writing-mode` property.
729                    if context.style().get_inherited_box().clone_writing_mode()
730                        == SpecifiedWritingMode::HorizontalTb
731                    {
732                        TextEmphasisShapeKeyword::Circle
733                    } else {
734                        TextEmphasisShapeKeyword::Sesame
735                    }
736                });
737                ComputedTextEmphasisStyle::Keyword { fill, shape }
738            },
739            TextEmphasisStyle::None => ComputedTextEmphasisStyle::None,
740            TextEmphasisStyle::String(ref s) => {
741                // FIXME(emilio): Doing this at computed value time seems wrong.
742                // The spec doesn't say that this should be a computed-value
743                // time operation. This is observable from getComputedStyle().
744                //
745                // Note that the first grapheme cluster boundary should always be the start of the string.
746                let first_grapheme_end = GraphemeClusterSegmenter::new()
747                    .segment_str(s)
748                    .nth(1)
749                    .unwrap_or(0);
750                ComputedTextEmphasisStyle::String(s[0..first_grapheme_end].to_string().into())
751            },
752        }
753    }
754
755    #[inline]
756    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
757        match *computed {
758            ComputedTextEmphasisStyle::Keyword { fill, shape } => TextEmphasisStyle::Keyword {
759                fill,
760                shape: Some(shape),
761            },
762            ComputedTextEmphasisStyle::None => TextEmphasisStyle::None,
763            ComputedTextEmphasisStyle::String(ref string) => {
764                TextEmphasisStyle::String(string.clone())
765            },
766        }
767    }
768}
769
770impl Parse for TextEmphasisStyle {
771    fn parse<'i, 't>(
772        _context: &ParserContext,
773        input: &mut Parser<'i, 't>,
774    ) -> Result<Self, ParseError<'i>> {
775        if input
776            .try_parse(|input| input.expect_ident_matching("none"))
777            .is_ok()
778        {
779            return Ok(TextEmphasisStyle::None);
780        }
781
782        if let Ok(s) = input.try_parse(|i| i.expect_string().map(|s| s.as_ref().to_owned())) {
783            // Handle <string>
784            return Ok(TextEmphasisStyle::String(s.into()));
785        }
786
787        // Handle a pair of keywords
788        let mut shape = input.try_parse(TextEmphasisShapeKeyword::parse).ok();
789        let fill = input.try_parse(TextEmphasisFillMode::parse).ok();
790        if shape.is_none() {
791            shape = input.try_parse(TextEmphasisShapeKeyword::parse).ok();
792        }
793
794        if shape.is_none() && fill.is_none() {
795            return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
796        }
797
798        // If a shape keyword is specified but neither filled nor open is
799        // specified, filled is assumed.
800        let fill = fill.unwrap_or(TextEmphasisFillMode::Filled);
801
802        // We cannot do the same because the default `<shape>` depends on the
803        // computed writing-mode.
804        Ok(TextEmphasisStyle::Keyword { fill, shape })
805    }
806}
807
808#[derive(
809    Clone,
810    Copy,
811    Debug,
812    Eq,
813    MallocSizeOf,
814    PartialEq,
815    Parse,
816    Serialize,
817    SpecifiedValueInfo,
818    ToCss,
819    ToComputedValue,
820    ToResolvedValue,
821    ToShmem,
822    ToTyped,
823)]
824#[repr(C)]
825#[css(bitflags(
826    single = "auto",
827    mixed = "over,under,left,right",
828    validate_mixed = "Self::validate_and_simplify"
829))]
830/// Values for text-emphasis-position:
831/// <https://drafts.csswg.org/css-text-decor/#text-emphasis-position-property>
832pub struct TextEmphasisPosition(u8);
833bitflags! {
834    impl TextEmphasisPosition: u8 {
835        /// Automatically choose mark position based on language.
836        const AUTO = 1 << 0;
837        /// Draw marks over the text in horizontal writing mode.
838        const OVER = 1 << 1;
839        /// Draw marks under the text in horizontal writing mode.
840        const UNDER = 1 << 2;
841        /// Draw marks to the left of the text in vertical writing mode.
842        const LEFT = 1 << 3;
843        /// Draw marks to the right of the text in vertical writing mode.
844        const RIGHT = 1 << 4;
845    }
846}
847
848impl TextEmphasisPosition {
849    fn validate_and_simplify(&mut self) -> bool {
850        // Require one but not both of 'over' and 'under'.
851        if self.intersects(Self::OVER) == self.intersects(Self::UNDER) {
852            return false;
853        }
854
855        // If 'left' is present, 'right' must be absent.
856        if self.intersects(Self::LEFT) {
857            return !self.intersects(Self::RIGHT);
858        }
859
860        self.remove(Self::RIGHT); // Right is the default
861        true
862    }
863}
864
865/// Values for the `word-break` property.
866#[repr(u8)]
867#[derive(
868    Clone,
869    Copy,
870    Debug,
871    Eq,
872    MallocSizeOf,
873    Parse,
874    PartialEq,
875    SpecifiedValueInfo,
876    ToComputedValue,
877    ToCss,
878    ToResolvedValue,
879    ToShmem,
880    ToTyped,
881)]
882#[allow(missing_docs)]
883pub enum WordBreak {
884    Normal,
885    BreakAll,
886    KeepAll,
887    /// The break-word value, needed for compat.
888    ///
889    /// Specifying `word-break: break-word` makes `overflow-wrap` behave as
890    /// `anywhere`, and `word-break` behave like `normal`.
891    #[cfg(feature = "gecko")]
892    BreakWord,
893}
894
895/// Values for the `text-justify` CSS property.
896#[repr(u8)]
897#[derive(
898    Clone,
899    Copy,
900    Debug,
901    Eq,
902    MallocSizeOf,
903    Parse,
904    PartialEq,
905    SpecifiedValueInfo,
906    ToComputedValue,
907    ToCss,
908    ToResolvedValue,
909    ToShmem,
910    ToTyped,
911)]
912#[allow(missing_docs)]
913pub enum TextJustify {
914    Auto,
915    None,
916    InterWord,
917    // See https://drafts.csswg.org/css-text-3/#valdef-text-justify-distribute
918    // and https://github.com/w3c/csswg-drafts/issues/6156 for the alias.
919    #[parse(aliases = "distribute")]
920    InterCharacter,
921}
922
923/// Values for the `-moz-control-character-visibility` CSS property.
924#[repr(u8)]
925#[derive(
926    Clone,
927    Copy,
928    Debug,
929    Eq,
930    MallocSizeOf,
931    Parse,
932    PartialEq,
933    SpecifiedValueInfo,
934    ToComputedValue,
935    ToCss,
936    ToResolvedValue,
937    ToShmem,
938    ToTyped,
939)]
940#[allow(missing_docs)]
941pub enum MozControlCharacterVisibility {
942    Hidden,
943    Visible,
944}
945
946#[cfg(feature = "gecko")]
947impl Default for MozControlCharacterVisibility {
948    fn default() -> Self {
949        if static_prefs::pref!("layout.css.control-characters.visible") {
950            Self::Visible
951        } else {
952            Self::Hidden
953        }
954    }
955}
956
957/// Values for the `line-break` property.
958#[repr(u8)]
959#[derive(
960    Clone,
961    Copy,
962    Debug,
963    Eq,
964    MallocSizeOf,
965    Parse,
966    PartialEq,
967    SpecifiedValueInfo,
968    ToComputedValue,
969    ToCss,
970    ToResolvedValue,
971    ToShmem,
972    ToTyped,
973)]
974#[allow(missing_docs)]
975pub enum LineBreak {
976    Auto,
977    Loose,
978    Normal,
979    Strict,
980    Anywhere,
981}
982
983/// Values for the `overflow-wrap` property.
984#[repr(u8)]
985#[derive(
986    Clone,
987    Copy,
988    Debug,
989    Eq,
990    MallocSizeOf,
991    Parse,
992    PartialEq,
993    SpecifiedValueInfo,
994    ToComputedValue,
995    ToCss,
996    ToResolvedValue,
997    ToShmem,
998    ToTyped,
999)]
1000#[allow(missing_docs)]
1001pub enum OverflowWrap {
1002    Normal,
1003    BreakWord,
1004    Anywhere,
1005}
1006
1007/// A specified value for the `text-indent` property
1008/// which takes the grammar of [<length-percentage>] && hanging? && each-line?
1009///
1010/// https://drafts.csswg.org/css-text/#propdef-text-indent
1011pub type TextIndent = GenericTextIndent<LengthPercentage>;
1012
1013impl Parse for TextIndent {
1014    fn parse<'i, 't>(
1015        context: &ParserContext,
1016        input: &mut Parser<'i, 't>,
1017    ) -> Result<Self, ParseError<'i>> {
1018        let mut length = None;
1019        let mut hanging = false;
1020        let mut each_line = false;
1021
1022        // The length-percentage and the two possible keywords can occur in any order.
1023        while !input.is_exhausted() {
1024            // If we haven't seen a length yet, try to parse one.
1025            if length.is_none() {
1026                if let Ok(len) = input
1027                    .try_parse(|i| LengthPercentage::parse_quirky(context, i, AllowQuirks::Yes))
1028                {
1029                    length = Some(len);
1030                    continue;
1031                }
1032            }
1033
1034            // Servo doesn't support the keywords, so just break and let the caller deal with it.
1035            if cfg!(feature = "servo") {
1036                break;
1037            }
1038
1039            // Check for the keywords (boolean flags).
1040            try_match_ident_ignore_ascii_case! { input,
1041                "hanging" if !hanging => hanging = true,
1042                "each-line" if !each_line => each_line = true,
1043            }
1044        }
1045
1046        // The length-percentage value is required for the declaration to be valid.
1047        if let Some(length) = length {
1048            Ok(Self {
1049                length,
1050                hanging,
1051                each_line,
1052            })
1053        } else {
1054            Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
1055        }
1056    }
1057}
1058
1059/// Implements text-decoration-skip-ink which takes the keywords auto | none | all
1060///
1061/// https://drafts.csswg.org/css-text-decor-4/#text-decoration-skip-ink-property
1062#[repr(u8)]
1063#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
1064#[derive(
1065    Clone,
1066    Copy,
1067    Debug,
1068    Eq,
1069    MallocSizeOf,
1070    Parse,
1071    PartialEq,
1072    SpecifiedValueInfo,
1073    ToComputedValue,
1074    ToCss,
1075    ToResolvedValue,
1076    ToShmem,
1077    ToTyped,
1078)]
1079#[allow(missing_docs)]
1080pub enum TextDecorationSkipInk {
1081    Auto,
1082    None,
1083    All,
1084}
1085
1086/// Implements type for `text-decoration-thickness` property
1087pub type TextDecorationLength = GenericTextDecorationLength<LengthPercentage>;
1088
1089impl TextDecorationLength {
1090    /// `Auto` value.
1091    #[inline]
1092    pub fn auto() -> Self {
1093        GenericTextDecorationLength::Auto
1094    }
1095
1096    /// Whether this is the `Auto` value.
1097    #[inline]
1098    pub fn is_auto(&self) -> bool {
1099        matches!(*self, GenericTextDecorationLength::Auto)
1100    }
1101}
1102
1103/// Implements type for `text-decoration-inset` property
1104pub type TextDecorationInset = GenericTextDecorationInset<Length>;
1105
1106impl TextDecorationInset {
1107    /// `Auto` value.
1108    #[inline]
1109    pub fn auto() -> Self {
1110        GenericTextDecorationInset::Auto
1111    }
1112
1113    /// Whether this is the `Auto` value.
1114    #[inline]
1115    pub fn is_auto(&self) -> bool {
1116        matches!(*self, GenericTextDecorationInset::Auto)
1117    }
1118}
1119
1120impl Parse for TextDecorationInset {
1121    fn parse<'i, 't>(
1122        ctx: &ParserContext,
1123        input: &mut Parser<'i, 't>,
1124    ) -> Result<Self, ParseError<'i>> {
1125        if let Ok(start) = input.try_parse(|i| Length::parse(ctx, i)) {
1126            let end = input.try_parse(|i| Length::parse(ctx, i));
1127            let end = end.unwrap_or_else(|_| start.clone());
1128            return Ok(TextDecorationInset::Length { start, end });
1129        }
1130        input.expect_ident_matching("auto")?;
1131        Ok(TextDecorationInset::Auto)
1132    }
1133}
1134
1135#[derive(
1136    Clone,
1137    Copy,
1138    Debug,
1139    Eq,
1140    MallocSizeOf,
1141    Parse,
1142    PartialEq,
1143    SpecifiedValueInfo,
1144    ToComputedValue,
1145    ToResolvedValue,
1146    ToShmem,
1147    ToTyped,
1148)]
1149#[css(bitflags(
1150    single = "auto",
1151    mixed = "from-font,under,left,right",
1152    validate_mixed = "Self::validate_mixed_flags",
1153))]
1154#[repr(C)]
1155/// Specified keyword values for the text-underline-position property.
1156/// (Non-exclusive, but not all combinations are allowed: the spec grammar gives
1157/// `auto | [ from-font | under ] || [ left | right ]`.)
1158/// https://drafts.csswg.org/css-text-decor-4/#text-underline-position-property
1159pub struct TextUnderlinePosition(u8);
1160bitflags! {
1161    impl TextUnderlinePosition: u8 {
1162        /// Use automatic positioning below the alphabetic baseline.
1163        const AUTO = 0;
1164        /// Use underline position from the first available font.
1165        const FROM_FONT = 1 << 0;
1166        /// Below the glyph box.
1167        const UNDER = 1 << 1;
1168        /// In vertical mode, place to the left of the text.
1169        const LEFT = 1 << 2;
1170        /// In vertical mode, place to the right of the text.
1171        const RIGHT = 1 << 3;
1172    }
1173}
1174
1175impl TextUnderlinePosition {
1176    fn validate_mixed_flags(&self) -> bool {
1177        if self.contains(Self::LEFT | Self::RIGHT) {
1178            // left and right can't be mixed together.
1179            return false;
1180        }
1181        if self.contains(Self::FROM_FONT | Self::UNDER) {
1182            // from-font and under can't be mixed together either.
1183            return false;
1184        }
1185        true
1186    }
1187}
1188
1189impl ToCss for TextUnderlinePosition {
1190    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
1191    where
1192        W: Write,
1193    {
1194        if self.is_empty() {
1195            return dest.write_str("auto");
1196        }
1197
1198        let mut writer = SequenceWriter::new(dest, " ");
1199        let mut any = false;
1200
1201        macro_rules! maybe_write {
1202            ($ident:ident => $str:expr) => {
1203                if self.contains(TextUnderlinePosition::$ident) {
1204                    any = true;
1205                    writer.raw_item($str)?;
1206                }
1207            };
1208        }
1209
1210        maybe_write!(FROM_FONT => "from-font");
1211        maybe_write!(UNDER => "under");
1212        maybe_write!(LEFT => "left");
1213        maybe_write!(RIGHT => "right");
1214
1215        debug_assert!(any);
1216
1217        Ok(())
1218    }
1219}
1220
1221/// Values for `ruby-position` property
1222#[repr(u8)]
1223#[derive(
1224    Clone,
1225    Copy,
1226    Debug,
1227    Eq,
1228    MallocSizeOf,
1229    PartialEq,
1230    ToComputedValue,
1231    ToResolvedValue,
1232    ToShmem,
1233    ToTyped,
1234)]
1235#[allow(missing_docs)]
1236pub enum RubyPosition {
1237    AlternateOver,
1238    AlternateUnder,
1239    Over,
1240    Under,
1241}
1242
1243impl Parse for RubyPosition {
1244    fn parse<'i, 't>(
1245        _context: &ParserContext,
1246        input: &mut Parser<'i, 't>,
1247    ) -> Result<RubyPosition, ParseError<'i>> {
1248        // Parse alternate before
1249        let alternate = input
1250            .try_parse(|i| i.expect_ident_matching("alternate"))
1251            .is_ok();
1252        if alternate && input.is_exhausted() {
1253            return Ok(RubyPosition::AlternateOver);
1254        }
1255        // Parse over / under
1256        let over = try_match_ident_ignore_ascii_case! { input,
1257            "over" => true,
1258            "under" => false,
1259        };
1260        // Parse alternate after
1261        let alternate = alternate
1262            || input
1263                .try_parse(|i| i.expect_ident_matching("alternate"))
1264                .is_ok();
1265
1266        Ok(match (over, alternate) {
1267            (true, true) => RubyPosition::AlternateOver,
1268            (false, true) => RubyPosition::AlternateUnder,
1269            (true, false) => RubyPosition::Over,
1270            (false, false) => RubyPosition::Under,
1271        })
1272    }
1273}
1274
1275impl ToCss for RubyPosition {
1276    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
1277    where
1278        W: Write,
1279    {
1280        dest.write_str(match self {
1281            RubyPosition::AlternateOver => "alternate",
1282            RubyPosition::AlternateUnder => "alternate under",
1283            RubyPosition::Over => "over",
1284            RubyPosition::Under => "under",
1285        })
1286    }
1287}
1288
1289impl SpecifiedValueInfo for RubyPosition {
1290    fn collect_completion_keywords(f: KeywordsCollectFn) {
1291        f(&["alternate", "over", "under"])
1292    }
1293}
1294
1295/// Specified value for the text-autospace property
1296/// which takes the grammar:
1297///     normal | <autospace> | auto
1298/// where:
1299///     <autospace> = no-autospace |
1300///                   [ ideograph-alpha || ideograph-numeric || punctuation ]
1301///                   || [ insert | replace ]
1302///
1303/// https://drafts.csswg.org/css-text-4/#text-autospace-property
1304///
1305/// Bug 1980111: 'replace' value is not supported yet.
1306#[derive(
1307    Clone,
1308    Copy,
1309    Debug,
1310    Eq,
1311    MallocSizeOf,
1312    Parse,
1313    PartialEq,
1314    Serialize,
1315    SpecifiedValueInfo,
1316    ToCss,
1317    ToComputedValue,
1318    ToResolvedValue,
1319    ToShmem,
1320    ToTyped,
1321)]
1322#[css(bitflags(
1323    single = "normal,auto,no-autospace",
1324    // Bug 1980111: add 'replace' to 'mixed' in the future so that it parses correctly.
1325    // Bug 1986500: add 'punctuation' to 'mixed' in the future so that it parses correctly.
1326    mixed = "ideograph-alpha,ideograph-numeric,insert",
1327    // Bug 1980111: Uncomment 'validate_mixed' to support 'replace' value.
1328    // validate_mixed = "Self::validate_mixed_flags",
1329))]
1330#[repr(C)]
1331pub struct TextAutospace(u8);
1332bitflags! {
1333    impl TextAutospace: u8 {
1334        /// No automatic space is inserted.
1335        const NO_AUTOSPACE = 0;
1336
1337        /// The user agent chooses a set of typographically high quality spacing values.
1338        const AUTO = 1 << 0;
1339
1340        /// Same behavior as ideograph-alpha ideograph-numeric.
1341        const NORMAL = 1 << 1;
1342
1343        /// 1/8ic space between ideographic characters and non-ideographic letters.
1344        const IDEOGRAPH_ALPHA = 1 << 2;
1345
1346        /// 1/8ic space between ideographic characters and non-ideographic decimal numerals.
1347        const IDEOGRAPH_NUMERIC = 1 << 3;
1348
1349        /* Bug 1986500: Uncomment the following to support the 'punctuation' value.
1350        /// Apply special spacing between letters and punctuation (French).
1351        const PUNCTUATION = 1 << 4;
1352        */
1353
1354        /// Auto-spacing is only inserted if no space character is present in the text.
1355        const INSERT = 1 << 5;
1356
1357        /* Bug 1980111: Uncomment the following to support 'replace' value.
1358        /// Auto-spacing may replace an existing U+0020 space with custom space.
1359        const REPLACE = 1 << 6;
1360        */
1361    }
1362}
1363
1364/* Bug 1980111: Uncomment the following to support 'replace' value.
1365impl TextAutospace {
1366    fn validate_mixed_flags(&self) -> bool {
1367        // It's not valid to have both INSERT and REPLACE set.
1368        !self.contains(TextAutospace::INSERT | TextAutospace::REPLACE)
1369    }
1370}
1371*/