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            if static_prefs::pref!("layout.css.text-indent-keywords.enabled") {
989                // Check for the keywords (boolean flags).
990                try_match_ident_ignore_ascii_case! { input,
991                    "hanging" if !hanging => hanging = true,
992                    "each-line" if !each_line => each_line = true,
993                }
994                continue;
995            }
996
997            // If we reach here, there must be something that we failed to parse;
998            // just break and let the caller deal with it.
999            break;
1000        }
1001
1002        // The length-percentage value is required for the declaration to be valid.
1003        if let Some(length) = length {
1004            Ok(Self {
1005                length,
1006                hanging,
1007                each_line,
1008            })
1009        } else {
1010            Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
1011        }
1012    }
1013}
1014
1015/// Implements text-decoration-skip-ink which takes the keywords auto | none | all
1016///
1017/// https://drafts.csswg.org/css-text-decor-4/#text-decoration-skip-ink-property
1018#[repr(u8)]
1019#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
1020#[derive(
1021    Clone,
1022    Copy,
1023    Debug,
1024    Eq,
1025    MallocSizeOf,
1026    Parse,
1027    PartialEq,
1028    SpecifiedValueInfo,
1029    ToComputedValue,
1030    ToCss,
1031    ToResolvedValue,
1032    ToShmem,
1033)]
1034#[allow(missing_docs)]
1035pub enum TextDecorationSkipInk {
1036    Auto,
1037    None,
1038    All,
1039}
1040
1041/// Implements type for `text-decoration-thickness` property
1042pub type TextDecorationLength = GenericTextDecorationLength<LengthPercentage>;
1043
1044impl TextDecorationLength {
1045    /// `Auto` value.
1046    #[inline]
1047    pub fn auto() -> Self {
1048        GenericTextDecorationLength::Auto
1049    }
1050
1051    /// Whether this is the `Auto` value.
1052    #[inline]
1053    pub fn is_auto(&self) -> bool {
1054        matches!(*self, GenericTextDecorationLength::Auto)
1055    }
1056}
1057
1058#[derive(
1059    Clone,
1060    Copy,
1061    Debug,
1062    Eq,
1063    MallocSizeOf,
1064    Parse,
1065    PartialEq,
1066    SpecifiedValueInfo,
1067    ToComputedValue,
1068    ToResolvedValue,
1069    ToShmem,
1070)]
1071#[css(bitflags(
1072    single = "auto",
1073    mixed = "from-font,under,left,right",
1074    validate_mixed = "Self::validate_mixed_flags",
1075))]
1076#[repr(C)]
1077/// Specified keyword values for the text-underline-position property.
1078/// (Non-exclusive, but not all combinations are allowed: the spec grammar gives
1079/// `auto | [ from-font | under ] || [ left | right ]`.)
1080/// https://drafts.csswg.org/css-text-decor-4/#text-underline-position-property
1081pub struct TextUnderlinePosition(u8);
1082bitflags! {
1083    impl TextUnderlinePosition: u8 {
1084        /// Use automatic positioning below the alphabetic baseline.
1085        const AUTO = 0;
1086        /// Use underline position from the first available font.
1087        const FROM_FONT = 1 << 0;
1088        /// Below the glyph box.
1089        const UNDER = 1 << 1;
1090        /// In vertical mode, place to the left of the text.
1091        const LEFT = 1 << 2;
1092        /// In vertical mode, place to the right of the text.
1093        const RIGHT = 1 << 3;
1094    }
1095}
1096
1097impl TextUnderlinePosition {
1098    fn validate_mixed_flags(&self) -> bool {
1099        if self.contains(Self::LEFT | Self::RIGHT) {
1100            // left and right can't be mixed together.
1101            return false;
1102        }
1103        if self.contains(Self::FROM_FONT | Self::UNDER) {
1104            // from-font and under can't be mixed together either.
1105            return false;
1106        }
1107        true
1108    }
1109}
1110
1111impl ToCss for TextUnderlinePosition {
1112    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
1113    where
1114        W: Write,
1115    {
1116        if self.is_empty() {
1117            return dest.write_str("auto");
1118        }
1119
1120        let mut writer = SequenceWriter::new(dest, " ");
1121        let mut any = false;
1122
1123        macro_rules! maybe_write {
1124            ($ident:ident => $str:expr) => {
1125                if self.contains(TextUnderlinePosition::$ident) {
1126                    any = true;
1127                    writer.raw_item($str)?;
1128                }
1129            };
1130        }
1131
1132        maybe_write!(FROM_FONT => "from-font");
1133        maybe_write!(UNDER => "under");
1134        maybe_write!(LEFT => "left");
1135        maybe_write!(RIGHT => "right");
1136
1137        debug_assert!(any);
1138
1139        Ok(())
1140    }
1141}
1142
1143/// Values for `ruby-position` property
1144#[repr(u8)]
1145#[derive(
1146    Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem,
1147)]
1148#[allow(missing_docs)]
1149pub enum RubyPosition {
1150    AlternateOver,
1151    AlternateUnder,
1152    Over,
1153    Under,
1154}
1155
1156impl Parse for RubyPosition {
1157    fn parse<'i, 't>(
1158        _context: &ParserContext,
1159        input: &mut Parser<'i, 't>,
1160    ) -> Result<RubyPosition, ParseError<'i>> {
1161        // Parse alternate before
1162        let alternate = input
1163            .try_parse(|i| i.expect_ident_matching("alternate"))
1164            .is_ok();
1165        if alternate && input.is_exhausted() {
1166            return Ok(RubyPosition::AlternateOver);
1167        }
1168        // Parse over / under
1169        let over = try_match_ident_ignore_ascii_case! { input,
1170            "over" => true,
1171            "under" => false,
1172        };
1173        // Parse alternate after
1174        let alternate = alternate ||
1175            input
1176                .try_parse(|i| i.expect_ident_matching("alternate"))
1177                .is_ok();
1178
1179        Ok(match (over, alternate) {
1180            (true, true) => RubyPosition::AlternateOver,
1181            (false, true) => RubyPosition::AlternateUnder,
1182            (true, false) => RubyPosition::Over,
1183            (false, false) => RubyPosition::Under,
1184        })
1185    }
1186}
1187
1188impl ToCss for RubyPosition {
1189    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
1190    where
1191        W: Write,
1192    {
1193        dest.write_str(match self {
1194            RubyPosition::AlternateOver => "alternate",
1195            RubyPosition::AlternateUnder => "alternate under",
1196            RubyPosition::Over => "over",
1197            RubyPosition::Under => "under",
1198        })
1199    }
1200}
1201
1202impl SpecifiedValueInfo for RubyPosition {
1203    fn collect_completion_keywords(f: KeywordsCollectFn) {
1204        f(&["alternate", "over", "under"])
1205    }
1206}