style/values/specified/
text.rs

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