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