Skip to main content

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