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