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(pre_hyphen_length);
145        Ok(Self {
146            total_word_length,
147            pre_hyphen_length,
148            post_hyphen_length,
149        })
150    }
151}
152
153impl Parse for InitialLetter {
154    fn parse<'i, 't>(
155        context: &ParserContext,
156        input: &mut Parser<'i, 't>,
157    ) -> Result<Self, ParseError<'i>> {
158        if input
159            .try_parse(|i| i.expect_ident_matching("normal"))
160            .is_ok()
161        {
162            return Ok(Self::normal());
163        }
164        let size = Number::parse_at_least_one(context, input)?;
165        let sink = input
166            .try_parse(|i| Integer::parse_positive(context, i))
167            .unwrap_or_else(|_| crate::Zero::zero());
168        Ok(Self { size, sink })
169    }
170}
171
172/// A generic value for the `text-overflow` property.
173#[derive(
174    Clone,
175    Debug,
176    Eq,
177    MallocSizeOf,
178    PartialEq,
179    Parse,
180    SpecifiedValueInfo,
181    ToComputedValue,
182    ToCss,
183    ToResolvedValue,
184    ToShmem,
185)]
186#[repr(C, u8)]
187pub enum TextOverflowSide {
188    /// Clip inline content.
189    Clip,
190    /// Render ellipsis to represent clipped inline content.
191    Ellipsis,
192    /// Render a given string to represent clipped inline content.
193    String(crate::values::AtomString),
194}
195
196#[derive(
197    Clone,
198    Debug,
199    Eq,
200    MallocSizeOf,
201    PartialEq,
202    SpecifiedValueInfo,
203    ToComputedValue,
204    ToResolvedValue,
205    ToShmem,
206    ToTyped,
207)]
208#[repr(C)]
209#[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)]
559#[typed(todo_derive_fields)]
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    MatchParent,
566    /// This is how we implement the following HTML behavior from
567    /// https://html.spec.whatwg.org/#tables-2:
568    ///
569    ///     User agents are expected to have a rule in their user agent style sheet
570    ///     that matches th elements that have a parent node whose computed value
571    ///     for the 'text-align' property is its initial value, whose declaration
572    ///     block consists of just a single declaration that sets the 'text-align'
573    ///     property to the value 'center'.
574    ///
575    /// Since selectors can't depend on the ancestor styles, we implement it with a
576    /// magic value that computes to the right thing. Since this is an
577    /// implementation detail, it shouldn't be exposed to web content.
578    #[parse(condition = "ParserContext::chrome_rules_enabled")]
579    MozCenterOrInherit,
580}
581
582impl ToComputedValue for TextAlign {
583    type ComputedValue = TextAlignKeyword;
584
585    #[inline]
586    fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue {
587        match *self {
588            TextAlign::Keyword(key) => key,
589            TextAlign::MatchParent => {
590                // on the root <html> element we should still respect the dir
591                // but the parent dir of that element is LTR even if it's <html dir=rtl>
592                // and will only be RTL if certain prefs have been set.
593                // In that case, the default behavior here will set it to left,
594                // but we want to set it to right -- instead set it to the default (`start`),
595                // which will do the right thing in this case (but not the general case)
596                if _context.builder.is_root_element {
597                    return TextAlignKeyword::Start;
598                }
599                let parent = _context
600                    .builder
601                    .get_parent_inherited_text()
602                    .clone_text_align();
603                let ltr = _context.builder.inherited_writing_mode().is_bidi_ltr();
604                match (parent, ltr) {
605                    (TextAlignKeyword::Start, true) => TextAlignKeyword::Left,
606                    (TextAlignKeyword::Start, false) => TextAlignKeyword::Right,
607                    (TextAlignKeyword::End, true) => TextAlignKeyword::Right,
608                    (TextAlignKeyword::End, false) => TextAlignKeyword::Left,
609                    _ => parent,
610                }
611            },
612            TextAlign::MozCenterOrInherit => {
613                let parent = _context
614                    .builder
615                    .get_parent_inherited_text()
616                    .clone_text_align();
617                if parent == TextAlignKeyword::Start {
618                    TextAlignKeyword::Center
619                } else {
620                    parent
621                }
622            },
623        }
624    }
625
626    #[inline]
627    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
628        TextAlign::Keyword(*computed)
629    }
630}
631
632fn fill_mode_is_default_and_shape_exists(
633    fill: &TextEmphasisFillMode,
634    shape: &Option<TextEmphasisShapeKeyword>,
635) -> bool {
636    shape.is_some() && fill.is_filled()
637}
638
639/// Specified value of text-emphasis-style property.
640///
641/// https://drafts.csswg.org/css-text-decor/#propdef-text-emphasis-style
642#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, ToTyped)]
643#[allow(missing_docs)]
644#[typed(todo_derive_fields)]
645pub enum TextEmphasisStyle {
646    /// [ <fill> || <shape> ]
647    Keyword {
648        #[css(contextual_skip_if = "fill_mode_is_default_and_shape_exists")]
649        fill: TextEmphasisFillMode,
650        shape: Option<TextEmphasisShapeKeyword>,
651    },
652    /// `none`
653    None,
654    /// `<string>` (of which only the first grapheme cluster will be used).
655    String(crate::OwnedStr),
656}
657
658/// Fill mode for the text-emphasis-style property
659#[derive(
660    Clone,
661    Copy,
662    Debug,
663    MallocSizeOf,
664    Parse,
665    PartialEq,
666    SpecifiedValueInfo,
667    ToCss,
668    ToComputedValue,
669    ToResolvedValue,
670    ToShmem,
671)]
672#[repr(u8)]
673pub enum TextEmphasisFillMode {
674    /// `filled`
675    Filled,
676    /// `open`
677    Open,
678}
679
680impl TextEmphasisFillMode {
681    /// Whether the value is `filled`.
682    #[inline]
683    pub fn is_filled(&self) -> bool {
684        matches!(*self, TextEmphasisFillMode::Filled)
685    }
686}
687
688/// Shape keyword for the text-emphasis-style property
689#[derive(
690    Clone,
691    Copy,
692    Debug,
693    Eq,
694    MallocSizeOf,
695    Parse,
696    PartialEq,
697    SpecifiedValueInfo,
698    ToCss,
699    ToComputedValue,
700    ToResolvedValue,
701    ToShmem,
702)]
703#[repr(u8)]
704pub enum TextEmphasisShapeKeyword {
705    /// `dot`
706    Dot,
707    /// `circle`
708    Circle,
709    /// `double-circle`
710    DoubleCircle,
711    /// `triangle`
712    Triangle,
713    /// `sesame`
714    Sesame,
715}
716
717impl ToComputedValue for TextEmphasisStyle {
718    type ComputedValue = ComputedTextEmphasisStyle;
719
720    #[inline]
721    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
722        match *self {
723            TextEmphasisStyle::Keyword { fill, shape } => {
724                let shape = shape.unwrap_or_else(|| {
725                    // FIXME(emilio, bug 1572958): This should set the
726                    // rule_cache_conditions properly.
727                    //
728                    // Also should probably use WritingMode::is_vertical rather
729                    // than the computed value of the `writing-mode` property.
730                    if context.style().get_inherited_box().clone_writing_mode()
731                        == SpecifiedWritingMode::HorizontalTb
732                    {
733                        TextEmphasisShapeKeyword::Circle
734                    } else {
735                        TextEmphasisShapeKeyword::Sesame
736                    }
737                });
738                ComputedTextEmphasisStyle::Keyword { fill, shape }
739            },
740            TextEmphasisStyle::None => ComputedTextEmphasisStyle::None,
741            TextEmphasisStyle::String(ref s) => {
742                // FIXME(emilio): Doing this at computed value time seems wrong.
743                // The spec doesn't say that this should be a computed-value
744                // time operation. This is observable from getComputedStyle().
745                //
746                // Note that the first grapheme cluster boundary should always be the start of the string.
747                let first_grapheme_end = GraphemeClusterSegmenter::new()
748                    .segment_str(s)
749                    .nth(1)
750                    .unwrap_or(0);
751                ComputedTextEmphasisStyle::String(s[0..first_grapheme_end].to_string().into())
752            },
753        }
754    }
755
756    #[inline]
757    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
758        match *computed {
759            ComputedTextEmphasisStyle::Keyword { fill, shape } => TextEmphasisStyle::Keyword {
760                fill,
761                shape: Some(shape),
762            },
763            ComputedTextEmphasisStyle::None => TextEmphasisStyle::None,
764            ComputedTextEmphasisStyle::String(ref string) => {
765                TextEmphasisStyle::String(string.clone())
766            },
767        }
768    }
769}
770
771impl Parse for TextEmphasisStyle {
772    fn parse<'i, 't>(
773        _context: &ParserContext,
774        input: &mut Parser<'i, 't>,
775    ) -> Result<Self, ParseError<'i>> {
776        if input
777            .try_parse(|input| input.expect_ident_matching("none"))
778            .is_ok()
779        {
780            return Ok(TextEmphasisStyle::None);
781        }
782
783        if let Ok(s) = input.try_parse(|i| i.expect_string().map(|s| s.as_ref().to_owned())) {
784            // Handle <string>
785            return Ok(TextEmphasisStyle::String(s.into()));
786        }
787
788        // Handle a pair of keywords
789        let mut shape = input.try_parse(TextEmphasisShapeKeyword::parse).ok();
790        let fill = input.try_parse(TextEmphasisFillMode::parse).ok();
791        if shape.is_none() {
792            shape = input.try_parse(TextEmphasisShapeKeyword::parse).ok();
793        }
794
795        if shape.is_none() && fill.is_none() {
796            return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
797        }
798
799        // If a shape keyword is specified but neither filled nor open is
800        // specified, filled is assumed.
801        let fill = fill.unwrap_or(TextEmphasisFillMode::Filled);
802
803        // We cannot do the same because the default `<shape>` depends on the
804        // computed writing-mode.
805        Ok(TextEmphasisStyle::Keyword { fill, shape })
806    }
807}
808
809#[derive(
810    Clone,
811    Copy,
812    Debug,
813    Eq,
814    MallocSizeOf,
815    PartialEq,
816    Parse,
817    Serialize,
818    SpecifiedValueInfo,
819    ToCss,
820    ToComputedValue,
821    ToResolvedValue,
822    ToShmem,
823    ToTyped,
824)]
825#[repr(C)]
826#[css(bitflags(
827    single = "auto",
828    mixed = "over,under,left,right",
829    validate_mixed = "Self::validate_and_simplify"
830))]
831/// Values for text-emphasis-position:
832/// <https://drafts.csswg.org/css-text-decor/#text-emphasis-position-property>
833pub struct TextEmphasisPosition(u8);
834bitflags! {
835    impl TextEmphasisPosition: u8 {
836        /// Automatically choose mark position based on language.
837        const AUTO = 1 << 0;
838        /// Draw marks over the text in horizontal writing mode.
839        const OVER = 1 << 1;
840        /// Draw marks under the text in horizontal writing mode.
841        const UNDER = 1 << 2;
842        /// Draw marks to the left of the text in vertical writing mode.
843        const LEFT = 1 << 3;
844        /// Draw marks to the right of the text in vertical writing mode.
845        const RIGHT = 1 << 4;
846    }
847}
848
849impl TextEmphasisPosition {
850    fn validate_and_simplify(&mut self) -> bool {
851        // Require one but not both of 'over' and 'under'.
852        if self.intersects(Self::OVER) == self.intersects(Self::UNDER) {
853            return false;
854        }
855
856        // If 'left' is present, 'right' must be absent.
857        if self.intersects(Self::LEFT) {
858            return !self.intersects(Self::RIGHT);
859        }
860
861        self.remove(Self::RIGHT); // Right is the default
862        true
863    }
864}
865
866/// Values for the `word-break` property.
867#[repr(u8)]
868#[derive(
869    Clone,
870    Copy,
871    Debug,
872    Eq,
873    MallocSizeOf,
874    Parse,
875    PartialEq,
876    SpecifiedValueInfo,
877    ToComputedValue,
878    ToCss,
879    ToResolvedValue,
880    ToShmem,
881    ToTyped,
882)]
883#[allow(missing_docs)]
884pub enum WordBreak {
885    Normal,
886    BreakAll,
887    KeepAll,
888    /// The break-word value, needed for compat.
889    ///
890    /// Specifying `word-break: break-word` makes `overflow-wrap` behave as
891    /// `anywhere`, and `word-break` behave like `normal`.
892    #[cfg(feature = "gecko")]
893    BreakWord,
894}
895
896/// Values for the `text-justify` CSS property.
897#[repr(u8)]
898#[derive(
899    Clone,
900    Copy,
901    Debug,
902    Eq,
903    MallocSizeOf,
904    Parse,
905    PartialEq,
906    SpecifiedValueInfo,
907    ToComputedValue,
908    ToCss,
909    ToResolvedValue,
910    ToShmem,
911    ToTyped,
912)]
913#[allow(missing_docs)]
914pub enum TextJustify {
915    Auto,
916    None,
917    InterWord,
918    // See https://drafts.csswg.org/css-text-3/#valdef-text-justify-distribute
919    // and https://github.com/w3c/csswg-drafts/issues/6156 for the alias.
920    #[parse(aliases = "distribute")]
921    InterCharacter,
922}
923
924/// Values for the `-moz-control-character-visibility` CSS property.
925#[repr(u8)]
926#[derive(
927    Clone,
928    Copy,
929    Debug,
930    Eq,
931    MallocSizeOf,
932    Parse,
933    PartialEq,
934    SpecifiedValueInfo,
935    ToComputedValue,
936    ToCss,
937    ToResolvedValue,
938    ToShmem,
939    ToTyped,
940)]
941#[allow(missing_docs)]
942pub enum MozControlCharacterVisibility {
943    Hidden,
944    Visible,
945}
946
947#[cfg(feature = "gecko")]
948impl Default for MozControlCharacterVisibility {
949    fn default() -> Self {
950        if static_prefs::pref!("layout.css.control-characters.visible") {
951            Self::Visible
952        } else {
953            Self::Hidden
954        }
955    }
956}
957
958/// Values for the `line-break` property.
959#[repr(u8)]
960#[derive(
961    Clone,
962    Copy,
963    Debug,
964    Eq,
965    MallocSizeOf,
966    Parse,
967    PartialEq,
968    SpecifiedValueInfo,
969    ToComputedValue,
970    ToCss,
971    ToResolvedValue,
972    ToShmem,
973    ToTyped,
974)]
975#[allow(missing_docs)]
976pub enum LineBreak {
977    Auto,
978    Loose,
979    Normal,
980    Strict,
981    Anywhere,
982}
983
984/// Values for the `overflow-wrap` property.
985#[repr(u8)]
986#[derive(
987    Clone,
988    Copy,
989    Debug,
990    Eq,
991    MallocSizeOf,
992    Parse,
993    PartialEq,
994    SpecifiedValueInfo,
995    ToComputedValue,
996    ToCss,
997    ToResolvedValue,
998    ToShmem,
999    ToTyped,
1000)]
1001#[allow(missing_docs)]
1002pub enum OverflowWrap {
1003    Normal,
1004    BreakWord,
1005    Anywhere,
1006}
1007
1008/// A specified value for the `text-indent` property
1009/// which takes the grammar of [<length-percentage>] && hanging? && each-line?
1010///
1011/// https://drafts.csswg.org/css-text/#propdef-text-indent
1012pub type TextIndent = GenericTextIndent<LengthPercentage>;
1013
1014impl Parse for TextIndent {
1015    fn parse<'i, 't>(
1016        context: &ParserContext,
1017        input: &mut Parser<'i, 't>,
1018    ) -> Result<Self, ParseError<'i>> {
1019        let mut length = None;
1020        let mut hanging = false;
1021        let mut each_line = false;
1022
1023        // The length-percentage and the two possible keywords can occur in any order.
1024        while !input.is_exhausted() {
1025            // If we haven't seen a length yet, try to parse one.
1026            if length.is_none() {
1027                if let Ok(len) = input
1028                    .try_parse(|i| LengthPercentage::parse_quirky(context, i, AllowQuirks::Yes))
1029                {
1030                    length = Some(len);
1031                    continue;
1032                }
1033            }
1034
1035            // Servo doesn't support the keywords, so just break and let the caller deal with it.
1036            if cfg!(feature = "servo") {
1037                break;
1038            }
1039
1040            // Check for the keywords (boolean flags).
1041            try_match_ident_ignore_ascii_case! { input,
1042                "hanging" if !hanging => hanging = true,
1043                "each-line" if !each_line => each_line = true,
1044            }
1045        }
1046
1047        // The length-percentage value is required for the declaration to be valid.
1048        if let Some(length) = length {
1049            Ok(Self {
1050                length,
1051                hanging,
1052                each_line,
1053            })
1054        } else {
1055            Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
1056        }
1057    }
1058}
1059
1060/// Implements text-decoration-skip-ink which takes the keywords auto | none | all
1061///
1062/// https://drafts.csswg.org/css-text-decor-4/#text-decoration-skip-ink-property
1063#[repr(u8)]
1064#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
1065#[derive(
1066    Clone,
1067    Copy,
1068    Debug,
1069    Eq,
1070    MallocSizeOf,
1071    Parse,
1072    PartialEq,
1073    SpecifiedValueInfo,
1074    ToComputedValue,
1075    ToCss,
1076    ToResolvedValue,
1077    ToShmem,
1078    ToTyped,
1079)]
1080#[allow(missing_docs)]
1081pub enum TextDecorationSkipInk {
1082    Auto,
1083    None,
1084    All,
1085}
1086
1087/// Implements type for `text-decoration-thickness` property
1088pub type TextDecorationLength = GenericTextDecorationLength<LengthPercentage>;
1089
1090impl TextDecorationLength {
1091    /// `Auto` value.
1092    #[inline]
1093    pub fn auto() -> Self {
1094        GenericTextDecorationLength::Auto
1095    }
1096
1097    /// Whether this is the `Auto` value.
1098    #[inline]
1099    pub fn is_auto(&self) -> bool {
1100        matches!(*self, GenericTextDecorationLength::Auto)
1101    }
1102}
1103
1104/// Implements type for `text-decoration-inset` property
1105pub type TextDecorationInset = GenericTextDecorationInset<Length>;
1106
1107impl TextDecorationInset {
1108    /// `Auto` value.
1109    #[inline]
1110    pub fn auto() -> Self {
1111        GenericTextDecorationInset::Auto
1112    }
1113
1114    /// Whether this is the `Auto` value.
1115    #[inline]
1116    pub fn is_auto(&self) -> bool {
1117        matches!(*self, GenericTextDecorationInset::Auto)
1118    }
1119}
1120
1121impl Parse for TextDecorationInset {
1122    fn parse<'i, 't>(
1123        ctx: &ParserContext,
1124        input: &mut Parser<'i, 't>,
1125    ) -> Result<Self, ParseError<'i>> {
1126        if let Ok(start) = input.try_parse(|i| Length::parse(ctx, i)) {
1127            let end = input.try_parse(|i| Length::parse(ctx, i));
1128            let end = end.unwrap_or_else(|_| start.clone());
1129            return Ok(TextDecorationInset::Length { start, end });
1130        }
1131        input.expect_ident_matching("auto")?;
1132        Ok(TextDecorationInset::Auto)
1133    }
1134}
1135
1136#[derive(
1137    Clone,
1138    Copy,
1139    Debug,
1140    Eq,
1141    MallocSizeOf,
1142    Parse,
1143    PartialEq,
1144    SpecifiedValueInfo,
1145    ToComputedValue,
1146    ToResolvedValue,
1147    ToShmem,
1148    ToTyped,
1149)]
1150#[css(bitflags(
1151    single = "auto",
1152    mixed = "from-font,under,left,right",
1153    validate_mixed = "Self::validate_mixed_flags",
1154))]
1155#[repr(C)]
1156/// Specified keyword values for the text-underline-position property.
1157/// (Non-exclusive, but not all combinations are allowed: the spec grammar gives
1158/// `auto | [ from-font | under ] || [ left | right ]`.)
1159/// https://drafts.csswg.org/css-text-decor-4/#text-underline-position-property
1160pub struct TextUnderlinePosition(u8);
1161bitflags! {
1162    impl TextUnderlinePosition: u8 {
1163        /// Use automatic positioning below the alphabetic baseline.
1164        const AUTO = 0;
1165        /// Use underline position from the first available font.
1166        const FROM_FONT = 1 << 0;
1167        /// Below the glyph box.
1168        const UNDER = 1 << 1;
1169        /// In vertical mode, place to the left of the text.
1170        const LEFT = 1 << 2;
1171        /// In vertical mode, place to the right of the text.
1172        const RIGHT = 1 << 3;
1173    }
1174}
1175
1176impl TextUnderlinePosition {
1177    fn validate_mixed_flags(&self) -> bool {
1178        if self.contains(Self::LEFT | Self::RIGHT) {
1179            // left and right can't be mixed together.
1180            return false;
1181        }
1182        if self.contains(Self::FROM_FONT | Self::UNDER) {
1183            // from-font and under can't be mixed together either.
1184            return false;
1185        }
1186        true
1187    }
1188}
1189
1190impl ToCss for TextUnderlinePosition {
1191    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
1192    where
1193        W: Write,
1194    {
1195        if self.is_empty() {
1196            return dest.write_str("auto");
1197        }
1198
1199        let mut writer = SequenceWriter::new(dest, " ");
1200        let mut any = false;
1201
1202        macro_rules! maybe_write {
1203            ($ident:ident => $str:expr) => {
1204                if self.contains(TextUnderlinePosition::$ident) {
1205                    any = true;
1206                    writer.raw_item($str)?;
1207                }
1208            };
1209        }
1210
1211        maybe_write!(FROM_FONT => "from-font");
1212        maybe_write!(UNDER => "under");
1213        maybe_write!(LEFT => "left");
1214        maybe_write!(RIGHT => "right");
1215
1216        debug_assert!(any);
1217
1218        Ok(())
1219    }
1220}
1221
1222/// Values for `ruby-position` property
1223#[repr(u8)]
1224#[derive(
1225    Clone,
1226    Copy,
1227    Debug,
1228    Eq,
1229    MallocSizeOf,
1230    PartialEq,
1231    ToComputedValue,
1232    ToResolvedValue,
1233    ToShmem,
1234    ToTyped,
1235)]
1236#[allow(missing_docs)]
1237pub enum RubyPosition {
1238    AlternateOver,
1239    AlternateUnder,
1240    Over,
1241    Under,
1242}
1243
1244impl Parse for RubyPosition {
1245    fn parse<'i, 't>(
1246        _context: &ParserContext,
1247        input: &mut Parser<'i, 't>,
1248    ) -> Result<RubyPosition, ParseError<'i>> {
1249        // Parse alternate before
1250        let alternate = input
1251            .try_parse(|i| i.expect_ident_matching("alternate"))
1252            .is_ok();
1253        if alternate && input.is_exhausted() {
1254            return Ok(RubyPosition::AlternateOver);
1255        }
1256        // Parse over / under
1257        let over = try_match_ident_ignore_ascii_case! { input,
1258            "over" => true,
1259            "under" => false,
1260        };
1261        // Parse alternate after
1262        let alternate = alternate
1263            || input
1264                .try_parse(|i| i.expect_ident_matching("alternate"))
1265                .is_ok();
1266
1267        Ok(match (over, alternate) {
1268            (true, true) => RubyPosition::AlternateOver,
1269            (false, true) => RubyPosition::AlternateUnder,
1270            (true, false) => RubyPosition::Over,
1271            (false, false) => RubyPosition::Under,
1272        })
1273    }
1274}
1275
1276impl ToCss for RubyPosition {
1277    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
1278    where
1279        W: Write,
1280    {
1281        dest.write_str(match self {
1282            RubyPosition::AlternateOver => "alternate",
1283            RubyPosition::AlternateUnder => "alternate under",
1284            RubyPosition::Over => "over",
1285            RubyPosition::Under => "under",
1286        })
1287    }
1288}
1289
1290impl SpecifiedValueInfo for RubyPosition {
1291    fn collect_completion_keywords(f: KeywordsCollectFn) {
1292        f(&["alternate", "over", "under"])
1293    }
1294}
1295
1296/// Specified value for the text-autospace property
1297/// which takes the grammar:
1298///     normal | <autospace> | auto
1299/// where:
1300///     <autospace> = no-autospace |
1301///                   [ ideograph-alpha || ideograph-numeric || punctuation ]
1302///                   || [ insert | replace ]
1303///
1304/// https://drafts.csswg.org/css-text-4/#text-autospace-property
1305///
1306/// Bug 1980111: 'replace' value is not supported yet.
1307#[derive(
1308    Clone,
1309    Copy,
1310    Debug,
1311    Eq,
1312    MallocSizeOf,
1313    Parse,
1314    PartialEq,
1315    Serialize,
1316    SpecifiedValueInfo,
1317    ToCss,
1318    ToComputedValue,
1319    ToResolvedValue,
1320    ToShmem,
1321    ToTyped,
1322)]
1323#[css(bitflags(
1324    single = "normal,auto,no-autospace",
1325    // Bug 1980111: add 'replace' to 'mixed' in the future so that it parses correctly.
1326    // Bug 1986500: add 'punctuation' to 'mixed' in the future so that it parses correctly.
1327    mixed = "ideograph-alpha,ideograph-numeric,insert",
1328    // Bug 1980111: Uncomment 'validate_mixed' to support 'replace' value.
1329    // validate_mixed = "Self::validate_mixed_flags",
1330))]
1331#[repr(C)]
1332pub struct TextAutospace(u8);
1333bitflags! {
1334    impl TextAutospace: u8 {
1335        /// No automatic space is inserted.
1336        const NO_AUTOSPACE = 0;
1337
1338        /// The user agent chooses a set of typographically high quality spacing values.
1339        const AUTO = 1 << 0;
1340
1341        /// Same behavior as ideograph-alpha ideograph-numeric.
1342        const NORMAL = 1 << 1;
1343
1344        /// 1/8ic space between ideographic characters and non-ideographic letters.
1345        const IDEOGRAPH_ALPHA = 1 << 2;
1346
1347        /// 1/8ic space between ideographic characters and non-ideographic decimal numerals.
1348        const IDEOGRAPH_NUMERIC = 1 << 3;
1349
1350        /* Bug 1986500: Uncomment the following to support the 'punctuation' value.
1351        /// Apply special spacing between letters and punctuation (French).
1352        const PUNCTUATION = 1 << 4;
1353        */
1354
1355        /// Auto-spacing is only inserted if no space character is present in the text.
1356        const INSERT = 1 << 5;
1357
1358        /* Bug 1980111: Uncomment the following to support 'replace' value.
1359        /// Auto-spacing may replace an existing U+0020 space with custom space.
1360        const REPLACE = 1 << 6;
1361        */
1362    }
1363}
1364
1365/* Bug 1980111: Uncomment the following to support 'replace' value.
1366impl TextAutospace {
1367    fn validate_mixed_flags(&self) -> bool {
1368        // It's not valid to have both INSERT and REPLACE set.
1369        !self.contains(TextAutospace::INSERT | TextAutospace::REPLACE)
1370    }
1371}
1372*/
1373#[derive(
1374    Clone,
1375    Copy,
1376    Debug,
1377    Eq,
1378    FromPrimitive,
1379    Hash,
1380    MallocSizeOf,
1381    Parse,
1382    PartialEq,
1383    SpecifiedValueInfo,
1384    ToComputedValue,
1385    ToCss,
1386    ToResolvedValue,
1387    ToShmem,
1388    ToTyped,
1389)]
1390#[repr(u8)]
1391/// Identifies specific font metrics for use in the <text-edge> typedef.
1392///
1393/// https://drafts.csswg.org/css-inline-3/#typedef-text-edge
1394pub enum TextEdgeKeyword {
1395    /// Use the text-over baseline/text-under baseline as the over/under edge.
1396    Text,
1397    /// Use the ideographic-over baseline/ideographic-under baseline as the over/under edge.
1398    Ideographic,
1399    /// Use the ideographic-ink-over baseline/ideographic-ink-under baseline as the over/under edge.
1400    IdeographicInk,
1401    /// Use the cap-height baseline as the over edge.
1402    Cap,
1403    /// Use the x-height baseline as the over edge.
1404    Ex,
1405    /// Use the alphabetic baseline as the under edge.
1406    Alphabetic,
1407}
1408
1409impl TextEdgeKeyword {
1410    fn is_valid_for_over(&self) -> bool {
1411        match self {
1412            TextEdgeKeyword::Text
1413            | TextEdgeKeyword::Ideographic
1414            | TextEdgeKeyword::IdeographicInk
1415            | TextEdgeKeyword::Cap
1416            | TextEdgeKeyword::Ex => true,
1417            _ => false,
1418        }
1419    }
1420
1421    fn is_valid_for_under(&self) -> bool {
1422        match self {
1423            TextEdgeKeyword::Text
1424            | TextEdgeKeyword::Ideographic
1425            | TextEdgeKeyword::IdeographicInk
1426            | TextEdgeKeyword::Alphabetic => true,
1427            _ => false,
1428        }
1429    }
1430}
1431
1432#[derive(
1433    Clone,
1434    Copy,
1435    Debug,
1436    Eq,
1437    Hash,
1438    MallocSizeOf,
1439    PartialEq,
1440    SpecifiedValueInfo,
1441    ToComputedValue,
1442    ToResolvedValue,
1443    ToShmem,
1444    ToTyped,
1445)]
1446#[repr(C)]
1447/// The <text-edge> typedef, used by the `line-fit-edge` and
1448/// `text-box-edge` properties.
1449///
1450/// The first value specifies the text over edge; the second value
1451/// specifies the text under edge. If only one value is specified,
1452/// both edges are assigned that same keyword if possible; else
1453/// text is assumed as the missing value.
1454///
1455/// https://drafts.csswg.org/css-inline-3/#typedef-text-edge
1456pub struct TextEdge {
1457    /// Font metric to use for the text over edge.
1458    pub over: TextEdgeKeyword,
1459    /// Font metric to use for the text under edge.
1460    pub under: TextEdgeKeyword,
1461}
1462
1463impl Parse for TextEdge {
1464    fn parse<'i, 't>(
1465        _context: &ParserContext,
1466        input: &mut Parser<'i, 't>,
1467    ) -> Result<TextEdge, ParseError<'i>> {
1468        let first = TextEdgeKeyword::parse(input)?;
1469
1470        if let Ok(second) = input.try_parse(TextEdgeKeyword::parse) {
1471            if !first.is_valid_for_over() || !second.is_valid_for_under() {
1472                return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
1473            }
1474
1475            return Ok(TextEdge {
1476                over: first,
1477                under: second,
1478            });
1479        }
1480
1481        // https://drafts.csswg.org/css-inline-3/#typedef-text-edge
1482        // > If only one value is specified, both edges are assigned that same
1483        // > keyword if possible; else 'text' is assumed as the missing value.
1484        match (first.is_valid_for_over(), first.is_valid_for_under()) {
1485            (true, true) => Ok(TextEdge {
1486                over: first,
1487                under: first,
1488            }),
1489            (true, false) => Ok(TextEdge {
1490                over: first,
1491                under: TextEdgeKeyword::Text,
1492            }),
1493            (false, true) => Ok(TextEdge {
1494                over: TextEdgeKeyword::Text,
1495                under: first,
1496            }),
1497            _ => unreachable!("Parsed keyword will be valid for at least one edge"),
1498        }
1499    }
1500}
1501
1502impl ToCss for TextEdge {
1503    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
1504    where
1505        W: Write,
1506    {
1507        match (self.over, self.under) {
1508            (over, TextEdgeKeyword::Text) if !over.is_valid_for_under() => over.to_css(dest),
1509            (TextEdgeKeyword::Text, under) if !under.is_valid_for_over() => under.to_css(dest),
1510            (over, under) => {
1511                over.to_css(dest)?;
1512
1513                if over != under {
1514                    dest.write_char(' ')?;
1515                    self.under.to_css(dest)?;
1516                }
1517
1518                Ok(())
1519            },
1520        }
1521    }
1522}
1523
1524#[derive(
1525    Clone,
1526    Copy,
1527    Debug,
1528    Eq,
1529    Hash,
1530    MallocSizeOf,
1531    Parse,
1532    PartialEq,
1533    SpecifiedValueInfo,
1534    ToComputedValue,
1535    ToCss,
1536    ToResolvedValue,
1537    ToShmem,
1538    ToTyped,
1539)]
1540#[repr(C, u8)]
1541/// Specified value for the `text-box-edge` property.
1542///
1543/// https://drafts.csswg.org/css-inline-3/#text-box-edge
1544pub enum TextBoxEdge {
1545    /// Uses the value of `line-fit-edge`, interpreting `leading` (the initial value) as `text`.
1546    Auto,
1547    /// Uses the specified font metrics.
1548    TextEdge(TextEdge),
1549}
1550
1551#[derive(
1552    Clone,
1553    Copy,
1554    Debug,
1555    Eq,
1556    MallocSizeOf,
1557    PartialEq,
1558    Parse,
1559    Serialize,
1560    SpecifiedValueInfo,
1561    ToCss,
1562    ToComputedValue,
1563    ToResolvedValue,
1564    ToShmem,
1565    ToTyped,
1566)]
1567#[css(bitflags(single = "none,trim-start,trim-end,trim-both"))]
1568#[repr(C)]
1569/// Specified value for the `text-box-trim` property.
1570///
1571/// https://drafts.csswg.org/css-inline-3/#text-box-edge
1572pub struct TextBoxTrim(u8);
1573bitflags! {
1574    impl TextBoxTrim: u8 {
1575        /// NONE
1576        const NONE = 0;
1577        /// TRIM_START
1578        const TRIM_START = 1 << 0;
1579        /// TRIM_END
1580        const TRIM_END = 1 << 1;
1581        /// TRIM_BOTH
1582        const TRIM_BOTH = Self::TRIM_START.0 | Self::TRIM_END.0;
1583    }
1584}
1585
1586impl TextBoxTrim {
1587    /// Returns the initial value of text-box-trim
1588    #[inline]
1589    pub fn none() -> Self {
1590        TextBoxTrim::NONE
1591    }
1592}