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