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)]
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)]
59pub struct LetterSpacing(pub Spacing);
60
61impl ToComputedValue for LetterSpacing {
62    type ComputedValue = computed::LetterSpacing;
63
64    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
65        use computed::text::GenericLetterSpacing;
66        match self.0 {
67            Spacing::Normal => GenericLetterSpacing(computed::LengthPercentage::zero()),
68            Spacing::Value(ref v) => GenericLetterSpacing(v.to_computed_value(context)),
69        }
70    }
71
72    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
73        if computed.0.is_zero() {
74            return LetterSpacing(Spacing::Normal);
75        }
76        LetterSpacing(Spacing::Value(ToComputedValue::from_computed_value(
77            &computed.0,
78        )))
79    }
80}
81
82/// A specified value for the `word-spacing` property.
83#[derive(
84    Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, ToTyped,
85)]
86pub struct WordSpacing(pub Spacing);
87
88impl ToComputedValue for WordSpacing {
89    type ComputedValue = computed::WordSpacing;
90
91    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
92        match self.0 {
93            Spacing::Normal => computed::LengthPercentage::zero(),
94            Spacing::Value(ref v) => v.to_computed_value(context),
95        }
96    }
97
98    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
99        WordSpacing(Spacing::Value(ToComputedValue::from_computed_value(
100            computed,
101        )))
102    }
103}
104
105/// A value for the `hyphenate-character` property.
106#[derive(
107    Clone,
108    Debug,
109    MallocSizeOf,
110    Parse,
111    PartialEq,
112    SpecifiedValueInfo,
113    ToComputedValue,
114    ToCss,
115    ToResolvedValue,
116    ToShmem,
117    ToTyped,
118)]
119#[repr(C, u8)]
120#[typed(todo_derive_fields)]
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_else(|_| pre_hyphen_length.clone());
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#[typed(todo_derive_fields)]
210/// text-overflow.
211/// When the specified value only has one side, that's the "second"
212/// side, and the sides are logical, so "second" means "end".  The
213/// start side is Clip in that case.
214///
215/// When the specified value has two sides, those are our "first"
216/// and "second" sides, and they are physical sides ("left" and
217/// "right").
218pub struct TextOverflow {
219    /// First side
220    pub first: TextOverflowSide,
221    /// Second side
222    pub second: TextOverflowSide,
223    /// True if the specified value only has one side.
224    pub sides_are_logical: bool,
225}
226
227impl Parse for TextOverflow {
228    fn parse<'i, 't>(
229        context: &ParserContext,
230        input: &mut Parser<'i, 't>,
231    ) -> Result<TextOverflow, ParseError<'i>> {
232        let first = TextOverflowSide::parse(context, input)?;
233        Ok(
234            if let Ok(second) = input.try_parse(|input| TextOverflowSide::parse(context, input)) {
235                Self {
236                    first,
237                    second,
238                    sides_are_logical: false,
239                }
240            } else {
241                Self {
242                    first: TextOverflowSide::Clip,
243                    second: first,
244                    sides_are_logical: true,
245                }
246            },
247        )
248    }
249}
250
251impl TextOverflow {
252    /// Returns the initial `text-overflow` value
253    pub fn get_initial_value() -> TextOverflow {
254        TextOverflow {
255            first: TextOverflowSide::Clip,
256            second: TextOverflowSide::Clip,
257            sides_are_logical: true,
258        }
259    }
260}
261
262impl ToCss for TextOverflow {
263    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
264    where
265        W: Write,
266    {
267        if self.sides_are_logical {
268            debug_assert_eq!(self.first, TextOverflowSide::Clip);
269            self.second.to_css(dest)?;
270        } else {
271            self.first.to_css(dest)?;
272            dest.write_char(' ')?;
273            self.second.to_css(dest)?;
274        }
275        Ok(())
276    }
277}
278
279#[derive(
280    Clone,
281    Copy,
282    Debug,
283    Eq,
284    MallocSizeOf,
285    PartialEq,
286    Parse,
287    Serialize,
288    SpecifiedValueInfo,
289    ToCss,
290    ToComputedValue,
291    ToResolvedValue,
292    ToShmem,
293    ToTyped,
294)]
295#[cfg_attr(
296    feature = "gecko",
297    css(bitflags(
298        single = "none,spelling-error,grammar-error",
299        mixed = "underline,overline,line-through,blink",
300    ))
301)]
302#[cfg_attr(
303    not(feature = "gecko"),
304    css(bitflags(single = "none", mixed = "underline,overline,line-through,blink",))
305)]
306#[repr(C)]
307/// Specified keyword values for the text-decoration-line property.
308pub struct TextDecorationLine(u8);
309bitflags! {
310    impl TextDecorationLine: u8 {
311        /// No text decoration line is specified.
312        const NONE = 0;
313        /// underline
314        const UNDERLINE = 1 << 0;
315        /// overline
316        const OVERLINE = 1 << 1;
317        /// line-through
318        const LINE_THROUGH = 1 << 2;
319        /// blink
320        const BLINK = 1 << 3;
321        /// spelling-error
322        const SPELLING_ERROR = 1 << 4;
323        /// grammar-error
324        const GRAMMAR_ERROR = 1 << 5;
325        /// Only set by presentation attributes
326        ///
327        /// Setting this will mean that text-decorations use the color
328        /// specified by `color` in quirks mode.
329        ///
330        /// For example, this gives <a href=foo><font color="red">text</font></a>
331        /// a red text decoration
332        #[cfg(feature = "gecko")]
333        const COLOR_OVERRIDE = 1 << 7;
334    }
335}
336
337impl Default for TextDecorationLine {
338    fn default() -> Self {
339        TextDecorationLine::NONE
340    }
341}
342
343impl TextDecorationLine {
344    #[inline]
345    /// Returns the initial value of text-decoration-line
346    pub fn none() -> Self {
347        TextDecorationLine::NONE
348    }
349}
350
351#[derive(
352    Clone,
353    Copy,
354    Debug,
355    Eq,
356    MallocSizeOf,
357    PartialEq,
358    SpecifiedValueInfo,
359    ToComputedValue,
360    ToCss,
361    ToResolvedValue,
362    ToShmem,
363)]
364#[repr(C)]
365/// Specified keyword values for case transforms in the text-transform property. (These are exclusive.)
366pub enum TextTransformCase {
367    /// No case transform.
368    None,
369    /// All uppercase.
370    Uppercase,
371    /// All lowercase.
372    Lowercase,
373    /// Capitalize each word.
374    Capitalize,
375    /// Automatic italicization of math variables.
376    #[cfg(feature = "gecko")]
377    MathAuto,
378}
379
380#[derive(
381    Clone,
382    Copy,
383    Debug,
384    Eq,
385    MallocSizeOf,
386    PartialEq,
387    Parse,
388    Serialize,
389    SpecifiedValueInfo,
390    ToCss,
391    ToComputedValue,
392    ToResolvedValue,
393    ToShmem,
394    ToTyped,
395)]
396#[cfg_attr(
397    feature = "gecko",
398    css(bitflags(
399        single = "none,math-auto",
400        mixed = "uppercase,lowercase,capitalize,full-width,full-size-kana",
401        validate_mixed = "Self::validate_mixed_flags",
402    ))
403)]
404#[cfg_attr(
405    not(feature = "gecko"),
406    css(bitflags(
407        single = "none",
408        mixed = "uppercase,lowercase,capitalize,full-width,full-size-kana",
409        validate_mixed = "Self::validate_mixed_flags",
410    ))
411)]
412#[repr(C)]
413/// Specified value for the text-transform property.
414/// (The spec grammar gives
415/// `none | math-auto | [capitalize | uppercase | lowercase] || full-width || full-size-kana`.)
416/// https://drafts.csswg.org/css-text-4/#text-transform-property
417pub struct TextTransform(u8);
418bitflags! {
419    impl TextTransform: u8 {
420        /// none
421        const NONE = 0;
422        /// All uppercase.
423        const UPPERCASE = 1 << 0;
424        /// All lowercase.
425        const LOWERCASE = 1 << 1;
426        /// Capitalize each word.
427        const CAPITALIZE = 1 << 2;
428        /// Automatic italicization of math variables.
429        #[cfg(feature = "gecko")]
430        const MATH_AUTO = 1 << 3;
431
432        /// All the case transforms, which are exclusive with each other.
433        #[cfg(feature = "gecko")]
434        const CASE_TRANSFORMS = Self::UPPERCASE.0 | Self::LOWERCASE.0 | Self::CAPITALIZE.0 | Self::MATH_AUTO.0;
435        /// All the case transforms, which are exclusive with each other.
436        #[cfg(feature = "servo")]
437        const CASE_TRANSFORMS = Self::UPPERCASE.0 | Self::LOWERCASE.0 | Self::CAPITALIZE.0;
438
439        /// full-width
440        const FULL_WIDTH = 1 << 4;
441        /// full-size-kana
442        const FULL_SIZE_KANA = 1 << 5;
443    }
444}
445
446impl TextTransform {
447    /// Returns the initial value of text-transform
448    #[inline]
449    pub fn none() -> Self {
450        Self::NONE
451    }
452
453    /// Returns whether the value is 'none'
454    #[inline]
455    pub fn is_none(self) -> bool {
456        self == Self::NONE
457    }
458
459    fn validate_mixed_flags(&self) -> bool {
460        let case = self.intersection(Self::CASE_TRANSFORMS);
461        // Case bits are exclusive with each other.
462        case.is_empty() || case.bits().is_power_of_two()
463    }
464
465    /// Returns the corresponding TextTransformCase.
466    pub fn case(&self) -> TextTransformCase {
467        match *self & Self::CASE_TRANSFORMS {
468            Self::NONE => TextTransformCase::None,
469            Self::UPPERCASE => TextTransformCase::Uppercase,
470            Self::LOWERCASE => TextTransformCase::Lowercase,
471            Self::CAPITALIZE => TextTransformCase::Capitalize,
472            #[cfg(feature = "gecko")]
473            Self::MATH_AUTO => TextTransformCase::MathAuto,
474            _ => unreachable!("Case bits are exclusive with each other"),
475        }
476    }
477}
478
479/// Specified and computed value of text-align-last.
480#[derive(
481    Clone,
482    Copy,
483    Debug,
484    Eq,
485    FromPrimitive,
486    Hash,
487    MallocSizeOf,
488    Parse,
489    PartialEq,
490    SpecifiedValueInfo,
491    ToComputedValue,
492    ToCss,
493    ToResolvedValue,
494    ToShmem,
495    ToTyped,
496)]
497#[allow(missing_docs)]
498#[repr(u8)]
499pub enum TextAlignLast {
500    Auto,
501    Start,
502    End,
503    Left,
504    Right,
505    Center,
506    Justify,
507}
508
509/// Specified value of text-align keyword value.
510#[derive(
511    Clone,
512    Copy,
513    Debug,
514    Eq,
515    FromPrimitive,
516    Hash,
517    MallocSizeOf,
518    Parse,
519    PartialEq,
520    SpecifiedValueInfo,
521    ToComputedValue,
522    ToCss,
523    ToResolvedValue,
524    ToShmem,
525    ToTyped,
526)]
527#[allow(missing_docs)]
528#[repr(u8)]
529pub enum TextAlignKeyword {
530    Start,
531    Left,
532    Right,
533    Center,
534    Justify,
535    End,
536    #[parse(aliases = "-webkit-center")]
537    MozCenter,
538    #[parse(aliases = "-webkit-left")]
539    MozLeft,
540    #[parse(aliases = "-webkit-right")]
541    MozRight,
542}
543
544/// Specified value of text-align property.
545#[derive(
546    Clone,
547    Copy,
548    Debug,
549    Eq,
550    Hash,
551    MallocSizeOf,
552    Parse,
553    PartialEq,
554    SpecifiedValueInfo,
555    ToCss,
556    ToShmem,
557    ToTyped,
558)]
559pub enum TextAlign {
560    /// Keyword value of text-align property.
561    Keyword(TextAlignKeyword),
562    /// `match-parent` value of text-align property. It has a different handling
563    /// unlike other keywords.
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            TextAlign::MatchParent => {
589                // on the root <html> element we should still respect the dir
590                // but the parent dir of that element is LTR even if it's <html dir=rtl>
591                // and will only be RTL if certain prefs have been set.
592                // In that case, the default behavior here will set it to left,
593                // but we want to set it to right -- instead set it to the default (`start`),
594                // which will do the right thing in this case (but not the general case)
595                if _context.builder.is_root_element {
596                    return TextAlignKeyword::Start;
597                }
598                let parent = _context
599                    .builder
600                    .get_parent_inherited_text()
601                    .clone_text_align();
602                let ltr = _context.builder.inherited_writing_mode().is_bidi_ltr();
603                match (parent, ltr) {
604                    (TextAlignKeyword::Start, true) => TextAlignKeyword::Left,
605                    (TextAlignKeyword::Start, false) => TextAlignKeyword::Right,
606                    (TextAlignKeyword::End, true) => TextAlignKeyword::Right,
607                    (TextAlignKeyword::End, false) => TextAlignKeyword::Left,
608                    _ => parent,
609                }
610            },
611            TextAlign::MozCenterOrInherit => {
612                let parent = _context
613                    .builder
614                    .get_parent_inherited_text()
615                    .clone_text_align();
616                if parent == TextAlignKeyword::Start {
617                    TextAlignKeyword::Center
618                } else {
619                    parent
620                }
621            },
622        }
623    }
624
625    #[inline]
626    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
627        TextAlign::Keyword(*computed)
628    }
629}
630
631fn fill_mode_is_default_and_shape_exists(
632    fill: &TextEmphasisFillMode,
633    shape: &Option<TextEmphasisShapeKeyword>,
634) -> bool {
635    shape.is_some() && fill.is_filled()
636}
637
638/// Specified value of text-emphasis-style property.
639///
640/// https://drafts.csswg.org/css-text-decor/#propdef-text-emphasis-style
641#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, ToTyped)]
642#[allow(missing_docs)]
643#[typed(todo_derive_fields)]
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*/
1372#[derive(
1373    Clone,
1374    Copy,
1375    Debug,
1376    Eq,
1377    FromPrimitive,
1378    Hash,
1379    MallocSizeOf,
1380    Parse,
1381    PartialEq,
1382    SpecifiedValueInfo,
1383    ToComputedValue,
1384    ToCss,
1385    ToResolvedValue,
1386    ToShmem,
1387    ToTyped,
1388)]
1389#[repr(u8)]
1390/// Identifies specific font metrics for use in the <text-edge> typedef.
1391///
1392/// https://drafts.csswg.org/css-inline-3/#typedef-text-edge
1393pub enum TextEdgeKeyword {
1394    /// Use the text-over baseline/text-under baseline as the over/under edge.
1395    Text,
1396    /// Use the ideographic-over baseline/ideographic-under baseline as the over/under edge.
1397    Ideographic,
1398    /// Use the ideographic-ink-over baseline/ideographic-ink-under baseline as the over/under edge.
1399    IdeographicInk,
1400    /// Use the cap-height baseline as the over edge.
1401    Cap,
1402    /// Use the x-height baseline as the over edge.
1403    Ex,
1404    /// Use the alphabetic baseline as the under edge.
1405    Alphabetic,
1406}
1407
1408impl TextEdgeKeyword {
1409    fn is_valid_for_over(&self) -> bool {
1410        match self {
1411            TextEdgeKeyword::Text
1412            | TextEdgeKeyword::Ideographic
1413            | TextEdgeKeyword::IdeographicInk
1414            | TextEdgeKeyword::Cap
1415            | TextEdgeKeyword::Ex => true,
1416            _ => false,
1417        }
1418    }
1419
1420    fn is_valid_for_under(&self) -> bool {
1421        match self {
1422            TextEdgeKeyword::Text
1423            | TextEdgeKeyword::Ideographic
1424            | TextEdgeKeyword::IdeographicInk
1425            | TextEdgeKeyword::Alphabetic => true,
1426            _ => false,
1427        }
1428    }
1429}
1430
1431#[derive(
1432    Clone,
1433    Copy,
1434    Debug,
1435    Eq,
1436    Hash,
1437    MallocSizeOf,
1438    PartialEq,
1439    SpecifiedValueInfo,
1440    ToComputedValue,
1441    ToResolvedValue,
1442    ToShmem,
1443    ToTyped,
1444)]
1445#[repr(C)]
1446/// The <text-edge> typedef, used by the `line-fit-edge` and
1447/// `text-box-edge` properties.
1448///
1449/// The first value specifies the text over edge; the second value
1450/// specifies the text under edge. If only one value is specified,
1451/// both edges are assigned that same keyword if possible; else
1452/// text is assumed as the missing value.
1453///
1454/// https://drafts.csswg.org/css-inline-3/#typedef-text-edge
1455pub struct TextEdge {
1456    /// Font metric to use for the text over edge.
1457    pub over: TextEdgeKeyword,
1458    /// Font metric to use for the text under edge.
1459    pub under: TextEdgeKeyword,
1460}
1461
1462impl Parse for TextEdge {
1463    fn parse<'i, 't>(
1464        _context: &ParserContext,
1465        input: &mut Parser<'i, 't>,
1466    ) -> Result<TextEdge, ParseError<'i>> {
1467        let first = TextEdgeKeyword::parse(input)?;
1468
1469        if let Ok(second) = input.try_parse(TextEdgeKeyword::parse) {
1470            if !first.is_valid_for_over() || !second.is_valid_for_under() {
1471                return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
1472            }
1473
1474            return Ok(TextEdge {
1475                over: first,
1476                under: second,
1477            });
1478        }
1479
1480        // https://drafts.csswg.org/css-inline-3/#typedef-text-edge
1481        // > If only one value is specified, both edges are assigned that same
1482        // > keyword if possible; else 'text' is assumed as the missing value.
1483        match (first.is_valid_for_over(), first.is_valid_for_under()) {
1484            (true, true) => Ok(TextEdge {
1485                over: first,
1486                under: first,
1487            }),
1488            (true, false) => Ok(TextEdge {
1489                over: first,
1490                under: TextEdgeKeyword::Text,
1491            }),
1492            (false, true) => Ok(TextEdge {
1493                over: TextEdgeKeyword::Text,
1494                under: first,
1495            }),
1496            _ => unreachable!("Parsed keyword will be valid for at least one edge"),
1497        }
1498    }
1499}
1500
1501impl ToCss for TextEdge {
1502    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
1503    where
1504        W: Write,
1505    {
1506        match (self.over, self.under) {
1507            (over, TextEdgeKeyword::Text) if !over.is_valid_for_under() => over.to_css(dest),
1508            (TextEdgeKeyword::Text, under) if !under.is_valid_for_over() => under.to_css(dest),
1509            (over, under) => {
1510                over.to_css(dest)?;
1511
1512                if over != under {
1513                    dest.write_char(' ')?;
1514                    self.under.to_css(dest)?;
1515                }
1516
1517                Ok(())
1518            },
1519        }
1520    }
1521}
1522
1523#[derive(
1524    Clone,
1525    Copy,
1526    Debug,
1527    Eq,
1528    Hash,
1529    MallocSizeOf,
1530    Parse,
1531    PartialEq,
1532    SpecifiedValueInfo,
1533    ToComputedValue,
1534    ToCss,
1535    ToResolvedValue,
1536    ToShmem,
1537    ToTyped,
1538)]
1539#[repr(C, u8)]
1540/// Specified value for the `text-box-edge` property.
1541///
1542/// https://drafts.csswg.org/css-inline-3/#text-box-edge
1543pub enum TextBoxEdge {
1544    /// Uses the value of `line-fit-edge`, interpreting `leading` (the initial value) as `text`.
1545    Auto,
1546    /// Uses the specified font metrics.
1547    TextEdge(TextEdge),
1548}
1549
1550#[derive(
1551    Clone,
1552    Copy,
1553    Debug,
1554    Eq,
1555    MallocSizeOf,
1556    PartialEq,
1557    Parse,
1558    Serialize,
1559    SpecifiedValueInfo,
1560    ToCss,
1561    ToComputedValue,
1562    ToResolvedValue,
1563    ToShmem,
1564    ToTyped,
1565)]
1566#[css(bitflags(single = "none,trim-start,trim-end,trim-both"))]
1567#[repr(C)]
1568/// Specified value for the `text-box-trim` property.
1569///
1570/// https://drafts.csswg.org/css-inline-3/#text-box-edge
1571pub struct TextBoxTrim(u8);
1572bitflags! {
1573    impl TextBoxTrim: u8 {
1574        /// NONE
1575        const NONE = 0;
1576        /// TRIM_START
1577        const TRIM_START = 1 << 0;
1578        /// TRIM_END
1579        const TRIM_END = 1 << 1;
1580        /// TRIM_BOTH
1581        const TRIM_BOTH = Self::TRIM_START.0 | Self::TRIM_END.0;
1582    }
1583}
1584
1585impl TextBoxTrim {
1586    /// Returns the initial value of text-box-trim
1587    #[inline]
1588    pub fn none() -> Self {
1589        TextBoxTrim::NONE
1590    }
1591}