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#[derive(
1064    Clone,
1065    Copy,
1066    Debug,
1067    Deserialize,
1068    Eq,
1069    MallocSizeOf,
1070    Parse,
1071    PartialEq,
1072    Serialize,
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<LengthPercentage>;
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
1121fn parse_inset_endpoint<'i, 't>(
1122    ctx: &ParserContext,
1123    input: &mut Parser<'i, 't>,
1124) -> Result<LengthPercentage, ParseError<'i>> {
1125    if !static_prefs::pref!("layout.css.text-decoration-inset-percentage.enabled") {
1126        Length::parse(ctx, input).map(|l| l.into())
1127    } else {
1128        LengthPercentage::parse(ctx, input)
1129    }
1130}
1131
1132impl Parse for TextDecorationInset {
1133    fn parse<'i, 't>(
1134        ctx: &ParserContext,
1135        input: &mut Parser<'i, 't>,
1136    ) -> Result<Self, ParseError<'i>> {
1137        if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() {
1138            return Ok(TextDecorationInset::Auto);
1139        }
1140
1141        let start = parse_inset_endpoint(ctx, input)?;
1142        let end = input
1143            .try_parse(|i| parse_inset_endpoint(ctx, i))
1144            .unwrap_or_else(|_| start.clone());
1145        Ok(TextDecorationInset::LengthPercentage { start, end })
1146    }
1147}
1148
1149#[derive(
1150    Clone,
1151    Copy,
1152    Debug,
1153    Eq,
1154    MallocSizeOf,
1155    Parse,
1156    PartialEq,
1157    SpecifiedValueInfo,
1158    ToComputedValue,
1159    ToResolvedValue,
1160    ToShmem,
1161    ToTyped,
1162)]
1163#[css(bitflags(
1164    single = "auto",
1165    mixed = "from-font,under,left,right",
1166    validate_mixed = "Self::validate_mixed_flags",
1167))]
1168#[repr(C)]
1169/// Specified keyword values for the text-underline-position property.
1170/// (Non-exclusive, but not all combinations are allowed: the spec grammar gives
1171/// `auto | [ from-font | under ] || [ left | right ]`.)
1172/// https://drafts.csswg.org/css-text-decor-4/#text-underline-position-property
1173pub struct TextUnderlinePosition(u8);
1174bitflags! {
1175    impl TextUnderlinePosition: u8 {
1176        /// Use automatic positioning below the alphabetic baseline.
1177        const AUTO = 0;
1178        /// Use underline position from the first available font.
1179        const FROM_FONT = 1 << 0;
1180        /// Below the glyph box.
1181        const UNDER = 1 << 1;
1182        /// In vertical mode, place to the left of the text.
1183        const LEFT = 1 << 2;
1184        /// In vertical mode, place to the right of the text.
1185        const RIGHT = 1 << 3;
1186    }
1187}
1188
1189impl TextUnderlinePosition {
1190    fn validate_mixed_flags(&self) -> bool {
1191        if self.contains(Self::LEFT | Self::RIGHT) {
1192            // left and right can't be mixed together.
1193            return false;
1194        }
1195        if self.contains(Self::FROM_FONT | Self::UNDER) {
1196            // from-font and under can't be mixed together either.
1197            return false;
1198        }
1199        true
1200    }
1201}
1202
1203impl ToCss for TextUnderlinePosition {
1204    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
1205    where
1206        W: Write,
1207    {
1208        if self.is_empty() {
1209            return dest.write_str("auto");
1210        }
1211
1212        let mut writer = SequenceWriter::new(dest, " ");
1213        let mut any = false;
1214
1215        macro_rules! maybe_write {
1216            ($ident:ident => $str:expr) => {
1217                if self.contains(TextUnderlinePosition::$ident) {
1218                    any = true;
1219                    writer.raw_item($str)?;
1220                }
1221            };
1222        }
1223
1224        maybe_write!(FROM_FONT => "from-font");
1225        maybe_write!(UNDER => "under");
1226        maybe_write!(LEFT => "left");
1227        maybe_write!(RIGHT => "right");
1228
1229        debug_assert!(any);
1230
1231        Ok(())
1232    }
1233}
1234
1235/// Values for `ruby-position` property
1236#[repr(u8)]
1237#[derive(
1238    Clone,
1239    Copy,
1240    Debug,
1241    Eq,
1242    MallocSizeOf,
1243    PartialEq,
1244    ToComputedValue,
1245    ToResolvedValue,
1246    ToShmem,
1247    ToTyped,
1248)]
1249#[allow(missing_docs)]
1250pub enum RubyPosition {
1251    AlternateOver,
1252    AlternateUnder,
1253    Over,
1254    Under,
1255}
1256
1257impl Parse for RubyPosition {
1258    fn parse<'i, 't>(
1259        _context: &ParserContext,
1260        input: &mut Parser<'i, 't>,
1261    ) -> Result<RubyPosition, ParseError<'i>> {
1262        // Parse alternate before
1263        let alternate = input
1264            .try_parse(|i| i.expect_ident_matching("alternate"))
1265            .is_ok();
1266        if alternate && input.is_exhausted() {
1267            return Ok(RubyPosition::AlternateOver);
1268        }
1269        // Parse over / under
1270        let over = try_match_ident_ignore_ascii_case! { input,
1271            "over" => true,
1272            "under" => false,
1273        };
1274        // Parse alternate after
1275        let alternate = alternate
1276            || input
1277                .try_parse(|i| i.expect_ident_matching("alternate"))
1278                .is_ok();
1279
1280        Ok(match (over, alternate) {
1281            (true, true) => RubyPosition::AlternateOver,
1282            (false, true) => RubyPosition::AlternateUnder,
1283            (true, false) => RubyPosition::Over,
1284            (false, false) => RubyPosition::Under,
1285        })
1286    }
1287}
1288
1289impl ToCss for RubyPosition {
1290    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
1291    where
1292        W: Write,
1293    {
1294        dest.write_str(match self {
1295            RubyPosition::AlternateOver => "alternate",
1296            RubyPosition::AlternateUnder => "alternate under",
1297            RubyPosition::Over => "over",
1298            RubyPosition::Under => "under",
1299        })
1300    }
1301}
1302
1303impl SpecifiedValueInfo for RubyPosition {
1304    fn collect_completion_keywords(f: KeywordsCollectFn) {
1305        f(&["alternate", "over", "under"])
1306    }
1307}
1308
1309/// Specified value for the text-autospace property
1310/// which takes the grammar:
1311///     normal | <autospace> | auto
1312/// where:
1313///     <autospace> = no-autospace |
1314///                   [ ideograph-alpha || ideograph-numeric || punctuation ]
1315///                   || [ insert | replace ]
1316///
1317/// https://drafts.csswg.org/css-text-4/#text-autospace-property
1318///
1319/// Bug 1980111: 'replace' value is not supported yet.
1320#[derive(
1321    Clone,
1322    Copy,
1323    Debug,
1324    Eq,
1325    MallocSizeOf,
1326    Parse,
1327    PartialEq,
1328    Serialize,
1329    SpecifiedValueInfo,
1330    ToCss,
1331    ToComputedValue,
1332    ToResolvedValue,
1333    ToShmem,
1334    ToTyped,
1335)]
1336#[css(bitflags(
1337    single = "normal,auto,no-autospace",
1338    // Bug 1980111: add 'replace' to 'mixed' in the future so that it parses correctly.
1339    // Bug 1986500: add 'punctuation' to 'mixed' in the future so that it parses correctly.
1340    mixed = "ideograph-alpha,ideograph-numeric,insert",
1341    // Bug 1980111: Uncomment 'validate_mixed' to support 'replace' value.
1342    // validate_mixed = "Self::validate_mixed_flags",
1343))]
1344#[repr(C)]
1345pub struct TextAutospace(u8);
1346bitflags! {
1347    impl TextAutospace: u8 {
1348        /// No automatic space is inserted.
1349        const NO_AUTOSPACE = 0;
1350
1351        /// The user agent chooses a set of typographically high quality spacing values.
1352        const AUTO = 1 << 0;
1353
1354        /// Same behavior as ideograph-alpha ideograph-numeric.
1355        const NORMAL = 1 << 1;
1356
1357        /// 1/8ic space between ideographic characters and non-ideographic letters.
1358        const IDEOGRAPH_ALPHA = 1 << 2;
1359
1360        /// 1/8ic space between ideographic characters and non-ideographic decimal numerals.
1361        const IDEOGRAPH_NUMERIC = 1 << 3;
1362
1363        /* Bug 1986500: Uncomment the following to support the 'punctuation' value.
1364        /// Apply special spacing between letters and punctuation (French).
1365        const PUNCTUATION = 1 << 4;
1366        */
1367
1368        /// Auto-spacing is only inserted if no space character is present in the text.
1369        const INSERT = 1 << 5;
1370
1371        /* Bug 1980111: Uncomment the following to support 'replace' value.
1372        /// Auto-spacing may replace an existing U+0020 space with custom space.
1373        const REPLACE = 1 << 6;
1374        */
1375    }
1376}
1377
1378/* Bug 1980111: Uncomment the following to support 'replace' value.
1379impl TextAutospace {
1380    fn validate_mixed_flags(&self) -> bool {
1381        // It's not valid to have both INSERT and REPLACE set.
1382        !self.contains(TextAutospace::INSERT | TextAutospace::REPLACE)
1383    }
1384}
1385*/
1386#[derive(
1387    Clone,
1388    Copy,
1389    Debug,
1390    Eq,
1391    FromPrimitive,
1392    Hash,
1393    MallocSizeOf,
1394    Parse,
1395    PartialEq,
1396    SpecifiedValueInfo,
1397    ToComputedValue,
1398    ToCss,
1399    ToResolvedValue,
1400    ToShmem,
1401    ToTyped,
1402)]
1403#[repr(u8)]
1404/// Identifies specific font metrics for use in the <text-edge> typedef.
1405///
1406/// https://drafts.csswg.org/css-inline-3/#typedef-text-edge
1407pub enum TextEdgeKeyword {
1408    /// Use the text-over baseline/text-under baseline as the over/under edge.
1409    Text,
1410    /// Use the ideographic-over baseline/ideographic-under baseline as the over/under edge.
1411    Ideographic,
1412    /// Use the ideographic-ink-over baseline/ideographic-ink-under baseline as the over/under edge.
1413    IdeographicInk,
1414    /// Use the cap-height baseline as the over edge.
1415    Cap,
1416    /// Use the x-height baseline as the over edge.
1417    Ex,
1418    /// Use the alphabetic baseline as the under edge.
1419    Alphabetic,
1420}
1421
1422impl TextEdgeKeyword {
1423    fn is_valid_for_over(&self) -> bool {
1424        match self {
1425            TextEdgeKeyword::Text
1426            | TextEdgeKeyword::Ideographic
1427            | TextEdgeKeyword::IdeographicInk
1428            | TextEdgeKeyword::Cap
1429            | TextEdgeKeyword::Ex => true,
1430            _ => false,
1431        }
1432    }
1433
1434    fn is_valid_for_under(&self) -> bool {
1435        match self {
1436            TextEdgeKeyword::Text
1437            | TextEdgeKeyword::Ideographic
1438            | TextEdgeKeyword::IdeographicInk
1439            | TextEdgeKeyword::Alphabetic => true,
1440            _ => false,
1441        }
1442    }
1443}
1444
1445#[derive(
1446    Clone,
1447    Copy,
1448    Debug,
1449    Eq,
1450    Hash,
1451    MallocSizeOf,
1452    PartialEq,
1453    SpecifiedValueInfo,
1454    ToComputedValue,
1455    ToResolvedValue,
1456    ToShmem,
1457    ToTyped,
1458)]
1459#[repr(C)]
1460/// The <text-edge> typedef, used by the `line-fit-edge` and
1461/// `text-box-edge` properties.
1462///
1463/// The first value specifies the text over edge; the second value
1464/// specifies the text under edge. If only one value is specified,
1465/// both edges are assigned that same keyword if possible; else
1466/// text is assumed as the missing value.
1467///
1468/// https://drafts.csswg.org/css-inline-3/#typedef-text-edge
1469pub struct TextEdge {
1470    /// Font metric to use for the text over edge.
1471    pub over: TextEdgeKeyword,
1472    /// Font metric to use for the text under edge.
1473    pub under: TextEdgeKeyword,
1474}
1475
1476impl Parse for TextEdge {
1477    fn parse<'i, 't>(
1478        _context: &ParserContext,
1479        input: &mut Parser<'i, 't>,
1480    ) -> Result<TextEdge, ParseError<'i>> {
1481        let first = TextEdgeKeyword::parse(input)?;
1482
1483        if let Ok(second) = input.try_parse(TextEdgeKeyword::parse) {
1484            if !first.is_valid_for_over() || !second.is_valid_for_under() {
1485                return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
1486            }
1487
1488            return Ok(TextEdge {
1489                over: first,
1490                under: second,
1491            });
1492        }
1493
1494        // https://drafts.csswg.org/css-inline-3/#typedef-text-edge
1495        // > If only one value is specified, both edges are assigned that same
1496        // > keyword if possible; else 'text' is assumed as the missing value.
1497        match (first.is_valid_for_over(), first.is_valid_for_under()) {
1498            (true, true) => Ok(TextEdge {
1499                over: first,
1500                under: first,
1501            }),
1502            (true, false) => Ok(TextEdge {
1503                over: first,
1504                under: TextEdgeKeyword::Text,
1505            }),
1506            (false, true) => Ok(TextEdge {
1507                over: TextEdgeKeyword::Text,
1508                under: first,
1509            }),
1510            _ => unreachable!("Parsed keyword will be valid for at least one edge"),
1511        }
1512    }
1513}
1514
1515impl ToCss for TextEdge {
1516    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
1517    where
1518        W: Write,
1519    {
1520        match (self.over, self.under) {
1521            (over, TextEdgeKeyword::Text) if !over.is_valid_for_under() => over.to_css(dest),
1522            (TextEdgeKeyword::Text, under) if !under.is_valid_for_over() => under.to_css(dest),
1523            (over, under) => {
1524                over.to_css(dest)?;
1525
1526                if over != under {
1527                    dest.write_char(' ')?;
1528                    self.under.to_css(dest)?;
1529                }
1530
1531                Ok(())
1532            },
1533        }
1534    }
1535}
1536
1537#[derive(
1538    Clone,
1539    Copy,
1540    Debug,
1541    Eq,
1542    Hash,
1543    MallocSizeOf,
1544    Parse,
1545    PartialEq,
1546    SpecifiedValueInfo,
1547    ToComputedValue,
1548    ToCss,
1549    ToResolvedValue,
1550    ToShmem,
1551    ToTyped,
1552)]
1553#[repr(C, u8)]
1554/// Specified value for the `text-box-edge` property.
1555///
1556/// https://drafts.csswg.org/css-inline-3/#text-box-edge
1557pub enum TextBoxEdge {
1558    /// Uses the value of `line-fit-edge`, interpreting `leading` (the initial value) as `text`.
1559    Auto,
1560    /// Uses the specified font metrics.
1561    TextEdge(TextEdge),
1562}
1563
1564#[derive(
1565    Clone,
1566    Copy,
1567    Debug,
1568    Eq,
1569    MallocSizeOf,
1570    PartialEq,
1571    Parse,
1572    Serialize,
1573    SpecifiedValueInfo,
1574    ToCss,
1575    ToComputedValue,
1576    ToResolvedValue,
1577    ToShmem,
1578    ToTyped,
1579)]
1580#[css(bitflags(single = "none,trim-start,trim-end,trim-both"))]
1581#[repr(C)]
1582/// Specified value for the `text-box-trim` property.
1583///
1584/// https://drafts.csswg.org/css-inline-3/#text-box-edge
1585pub struct TextBoxTrim(u8);
1586bitflags! {
1587    impl TextBoxTrim: u8 {
1588        /// NONE
1589        const NONE = 0;
1590        /// TRIM_START
1591        const TRIM_START = 1 << 0;
1592        /// TRIM_END
1593        const TRIM_END = 1 << 1;
1594        /// TRIM_BOTH
1595        const TRIM_BOTH = Self::TRIM_START.0 | Self::TRIM_END.0;
1596    }
1597}
1598
1599impl TextBoxTrim {
1600    /// Returns the initial value of text-box-trim
1601    #[inline]
1602    pub fn none() -> Self {
1603        TextBoxTrim::NONE
1604    }
1605}