style/values/specified/
text.rs

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