/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Specified types for text properties. use crate::parser::{Parse, ParserContext}; use crate::properties::longhands::writing_mode::computed_value::T as SpecifiedWritingMode; use crate::values::computed::text::TextEmphasisStyle as ComputedTextEmphasisStyle; use crate::values::computed::{Context, ToComputedValue}; use crate::values::generics::text::{ GenericInitialLetter, GenericTextDecorationLength, GenericTextIndent, Spacing, }; use crate::values::specified::length::{Length, LengthPercentage}; use crate::values::specified::{AllowQuirks, Integer, Number}; use cssparser::Parser; use icu_segmenter::GraphemeClusterSegmenter; use std::fmt::{self, Write}; use style_traits::values::SequenceWriter; use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; use style_traits::{KeywordsCollectFn, SpecifiedValueInfo}; /// A specified type for the `initial-letter` property. pub type InitialLetter = GenericInitialLetter; /// A specified value for the `letter-spacing` property. pub type LetterSpacing = Spacing; /// A specified value for the `word-spacing` property. pub type WordSpacing = Spacing; /// A value for the `hyphenate-character` property. #[derive( Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] #[repr(C, u8)] pub enum HyphenateCharacter { /// `auto` Auto, /// `` String(crate::OwnedStr), } impl Parse for InitialLetter { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { if input .try_parse(|i| i.expect_ident_matching("normal")) .is_ok() { return Ok(Self::normal()); } let size = Number::parse_at_least_one(context, input)?; let sink = input .try_parse(|i| Integer::parse_positive(context, i)) .unwrap_or_else(|_| crate::Zero::zero()); Ok(Self { size, sink }) } } impl Parse for LetterSpacing { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { Spacing::parse_with(context, input, |c, i| { Length::parse_quirky(c, i, AllowQuirks::Yes) }) } } impl Parse for WordSpacing { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { Spacing::parse_with(context, input, |c, i| { LengthPercentage::parse_quirky(c, i, AllowQuirks::Yes) }) } } /// A generic value for the `text-overflow` property. #[derive( Clone, Debug, Eq, MallocSizeOf, PartialEq, Parse, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] #[repr(C, u8)] pub enum TextOverflowSide { /// Clip inline content. Clip, /// Render ellipsis to represent clipped inline content. Ellipsis, /// Render a given string to represent clipped inline content. String(crate::values::AtomString), } #[derive( Clone, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem, )] #[repr(C)] /// text-overflow. /// When the specified value only has one side, that's the "second" /// side, and the sides are logical, so "second" means "end". The /// start side is Clip in that case. /// /// When the specified value has two sides, those are our "first" /// and "second" sides, and they are physical sides ("left" and /// "right"). pub struct TextOverflow { /// First side pub first: TextOverflowSide, /// Second side pub second: TextOverflowSide, /// True if the specified value only has one side. pub sides_are_logical: bool, } impl Parse for TextOverflow { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let first = TextOverflowSide::parse(context, input)?; Ok( if let Ok(second) = input.try_parse(|input| TextOverflowSide::parse(context, input)) { Self { first, second, sides_are_logical: false, } } else { Self { first: TextOverflowSide::Clip, second: first, sides_are_logical: true, } }, ) } } impl TextOverflow { /// Returns the initial `text-overflow` value pub fn get_initial_value() -> TextOverflow { TextOverflow { first: TextOverflowSide::Clip, second: TextOverflowSide::Clip, sides_are_logical: true, } } } impl ToCss for TextOverflow { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: Write, { if self.sides_are_logical { debug_assert_eq!(self.first, TextOverflowSide::Clip); self.second.to_css(dest)?; } else { self.first.to_css(dest)?; dest.write_char(' ')?; self.second.to_css(dest)?; } Ok(()) } } #[derive( Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, Parse, Serialize, SpecifiedValueInfo, ToCss, ToComputedValue, ToResolvedValue, ToShmem, )] #[css(bitflags(single = "none", mixed = "underline,overline,line-through,blink"))] #[repr(C)] /// Specified keyword values for the text-decoration-line property. pub struct TextDecorationLine(u8); bitflags! { impl TextDecorationLine: u8 { /// No text decoration line is specified. const NONE = 0; /// underline const UNDERLINE = 1 << 0; /// overline const OVERLINE = 1 << 1; /// line-through const LINE_THROUGH = 1 << 2; /// blink const BLINK = 1 << 3; /// Only set by presentation attributes /// /// Setting this will mean that text-decorations use the color /// specified by `color` in quirks mode. /// /// For example, this gives text /// a red text decoration #[cfg(feature = "gecko")] const COLOR_OVERRIDE = 0x10; } } impl Default for TextDecorationLine { fn default() -> Self { TextDecorationLine::NONE } } impl TextDecorationLine { #[inline] /// Returns the initial value of text-decoration-line pub fn none() -> Self { TextDecorationLine::NONE } } #[derive( Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] #[repr(C)] /// Specified keyword values for case transforms in the text-transform property. (These are exclusive.) pub enum TextTransformCase { /// No case transform. None, /// All uppercase. Uppercase, /// All lowercase. Lowercase, /// Capitalize each word. Capitalize, /// Automatic italicization of math variables. #[cfg(feature = "gecko")] MathAuto, } #[derive( Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, Parse, Serialize, SpecifiedValueInfo, ToCss, ToComputedValue, ToResolvedValue, ToShmem, )] #[cfg_attr(feature = "gecko", css(bitflags( single = "none,math-auto", mixed = "uppercase,lowercase,capitalize,full-width,full-size-kana", validate_mixed = "Self::validate_mixed_flags", )))] #[cfg_attr(not(feature = "gecko"), css(bitflags( single = "none", mixed = "uppercase,lowercase,capitalize,full-width,full-size-kana", validate_mixed = "Self::validate_mixed_flags", )))] #[repr(C)] /// Specified value for the text-transform property. /// (The spec grammar gives /// `none | math-auto | [capitalize | uppercase | lowercase] || full-width || full-size-kana`.) /// https://drafts.csswg.org/css-text-4/#text-transform-property pub struct TextTransform(u8); bitflags! { impl TextTransform: u8 { /// none const NONE = 0; /// All uppercase. const UPPERCASE = 1 << 0; /// All lowercase. const LOWERCASE = 1 << 1; /// Capitalize each word. const CAPITALIZE = 1 << 2; /// Automatic italicization of math variables. #[cfg(feature = "gecko")] const MATH_AUTO = 1 << 3; /// All the case transforms, which are exclusive with each other. #[cfg(feature = "gecko")] const CASE_TRANSFORMS = Self::UPPERCASE.0 | Self::LOWERCASE.0 | Self::CAPITALIZE.0 | Self::MATH_AUTO.0; /// All the case transforms, which are exclusive with each other. #[cfg(feature = "servo")] const CASE_TRANSFORMS = Self::UPPERCASE.0 | Self::LOWERCASE.0 | Self::CAPITALIZE.0; /// full-width const FULL_WIDTH = 1 << 4; /// full-size-kana const FULL_SIZE_KANA = 1 << 5; } } impl TextTransform { /// Returns the initial value of text-transform #[inline] pub fn none() -> Self { Self::NONE } /// Returns whether the value is 'none' #[inline] pub fn is_none(self) -> bool { self == Self::NONE } fn validate_mixed_flags(&self) -> bool { let case = self.intersection(Self::CASE_TRANSFORMS); // Case bits are exclusive with each other. case.is_empty() || case.bits().is_power_of_two() } /// Returns the corresponding TextTransformCase. pub fn case(&self) -> TextTransformCase { match *self & Self::CASE_TRANSFORMS { Self::NONE => TextTransformCase::None, Self::UPPERCASE => TextTransformCase::Uppercase, Self::LOWERCASE => TextTransformCase::Lowercase, Self::CAPITALIZE => TextTransformCase::Capitalize, #[cfg(feature = "gecko")] Self::MATH_AUTO => TextTransformCase::MathAuto, _ => unreachable!("Case bits are exclusive with each other"), } } } /// Specified and computed value of text-align-last. #[derive( Clone, Copy, Debug, Eq, FromPrimitive, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] #[allow(missing_docs)] #[repr(u8)] pub enum TextAlignLast { Auto, Start, End, Left, Right, Center, Justify, } /// Specified value of text-align keyword value. #[derive( Clone, Copy, Debug, Eq, FromPrimitive, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] #[allow(missing_docs)] #[repr(u8)] pub enum TextAlignKeyword { Start, Left, Right, Center, Justify, End, #[parse(aliases = "-webkit-center")] MozCenter, #[parse(aliases = "-webkit-left")] MozLeft, #[parse(aliases = "-webkit-right")] MozRight, } /// Specified value of text-align property. #[derive( Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, )] pub enum TextAlign { /// Keyword value of text-align property. Keyword(TextAlignKeyword), /// `match-parent` value of text-align property. It has a different handling /// unlike other keywords. #[cfg(feature = "gecko")] MatchParent, /// This is how we implement the following HTML behavior from /// https://html.spec.whatwg.org/#tables-2: /// /// User agents are expected to have a rule in their user agent style sheet /// that matches th elements that have a parent node whose computed value /// for the 'text-align' property is its initial value, whose declaration /// block consists of just a single declaration that sets the 'text-align' /// property to the value 'center'. /// /// Since selectors can't depend on the ancestor styles, we implement it with a /// magic value that computes to the right thing. Since this is an /// implementation detail, it shouldn't be exposed to web content. #[parse(condition = "ParserContext::chrome_rules_enabled")] MozCenterOrInherit, } impl ToComputedValue for TextAlign { type ComputedValue = TextAlignKeyword; #[inline] fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue { match *self { TextAlign::Keyword(key) => key, #[cfg(feature = "gecko")] TextAlign::MatchParent => { // on the root element we should still respect the dir // but the parent dir of that element is LTR even if it's // and will only be RTL if certain prefs have been set. // In that case, the default behavior here will set it to left, // but we want to set it to right -- instead set it to the default (`start`), // which will do the right thing in this case (but not the general case) if _context.builder.is_root_element { return TextAlignKeyword::Start; } let parent = _context .builder .get_parent_inherited_text() .clone_text_align(); let ltr = _context.builder.inherited_writing_mode().is_bidi_ltr(); match (parent, ltr) { (TextAlignKeyword::Start, true) => TextAlignKeyword::Left, (TextAlignKeyword::Start, false) => TextAlignKeyword::Right, (TextAlignKeyword::End, true) => TextAlignKeyword::Right, (TextAlignKeyword::End, false) => TextAlignKeyword::Left, _ => parent, } }, TextAlign::MozCenterOrInherit => { let parent = _context .builder .get_parent_inherited_text() .clone_text_align(); if parent == TextAlignKeyword::Start { TextAlignKeyword::Center } else { parent } }, } } #[inline] fn from_computed_value(computed: &Self::ComputedValue) -> Self { TextAlign::Keyword(*computed) } } fn fill_mode_is_default_and_shape_exists( fill: &TextEmphasisFillMode, shape: &Option, ) -> bool { shape.is_some() && fill.is_filled() } /// Specified value of text-emphasis-style property. /// /// https://drafts.csswg.org/css-text-decor/#propdef-text-emphasis-style #[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] #[allow(missing_docs)] pub enum TextEmphasisStyle { /// [ || ] Keyword { #[css(contextual_skip_if = "fill_mode_is_default_and_shape_exists")] fill: TextEmphasisFillMode, shape: Option, }, /// `none` None, /// `` (of which only the first grapheme cluster will be used). String(crate::OwnedStr), } /// Fill mode for the text-emphasis-style property #[derive( Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToComputedValue, ToResolvedValue, ToShmem, )] #[repr(u8)] pub enum TextEmphasisFillMode { /// `filled` Filled, /// `open` Open, } impl TextEmphasisFillMode { /// Whether the value is `filled`. #[inline] pub fn is_filled(&self) -> bool { matches!(*self, TextEmphasisFillMode::Filled) } } /// Shape keyword for the text-emphasis-style property #[derive( Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToComputedValue, ToResolvedValue, ToShmem, )] #[repr(u8)] pub enum TextEmphasisShapeKeyword { /// `dot` Dot, /// `circle` Circle, /// `double-circle` DoubleCircle, /// `triangle` Triangle, /// `sesame` Sesame, } impl ToComputedValue for TextEmphasisStyle { type ComputedValue = ComputedTextEmphasisStyle; #[inline] fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { match *self { TextEmphasisStyle::Keyword { fill, shape } => { let shape = shape.unwrap_or_else(|| { // FIXME(emilio, bug 1572958): This should set the // rule_cache_conditions properly. // // Also should probably use WritingMode::is_vertical rather // than the computed value of the `writing-mode` property. if context.style().get_inherited_box().clone_writing_mode() == SpecifiedWritingMode::HorizontalTb { TextEmphasisShapeKeyword::Circle } else { TextEmphasisShapeKeyword::Sesame } }); ComputedTextEmphasisStyle::Keyword { fill, shape } }, TextEmphasisStyle::None => ComputedTextEmphasisStyle::None, TextEmphasisStyle::String(ref s) => { // FIXME(emilio): Doing this at computed value time seems wrong. // The spec doesn't say that this should be a computed-value // time operation. This is observable from getComputedStyle(). // // Note that the first grapheme cluster boundary should always be the start of the string. let first_grapheme_end = GraphemeClusterSegmenter::new() .segment_str(s) .nth(1) .unwrap_or(0); ComputedTextEmphasisStyle::String(s[0..first_grapheme_end].to_string().into()) }, } } #[inline] fn from_computed_value(computed: &Self::ComputedValue) -> Self { match *computed { ComputedTextEmphasisStyle::Keyword { fill, shape } => TextEmphasisStyle::Keyword { fill, shape: Some(shape), }, ComputedTextEmphasisStyle::None => TextEmphasisStyle::None, ComputedTextEmphasisStyle::String(ref string) => { TextEmphasisStyle::String(string.clone()) }, } } } impl Parse for TextEmphasisStyle { fn parse<'i, 't>( _context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { if input .try_parse(|input| input.expect_ident_matching("none")) .is_ok() { return Ok(TextEmphasisStyle::None); } if let Ok(s) = input.try_parse(|i| i.expect_string().map(|s| s.as_ref().to_owned())) { // Handle return Ok(TextEmphasisStyle::String(s.into())); } // Handle a pair of keywords let mut shape = input.try_parse(TextEmphasisShapeKeyword::parse).ok(); let fill = input.try_parse(TextEmphasisFillMode::parse).ok(); if shape.is_none() { shape = input.try_parse(TextEmphasisShapeKeyword::parse).ok(); } if shape.is_none() && fill.is_none() { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } // If a shape keyword is specified but neither filled nor open is // specified, filled is assumed. let fill = fill.unwrap_or(TextEmphasisFillMode::Filled); // We cannot do the same because the default `` depends on the // computed writing-mode. Ok(TextEmphasisStyle::Keyword { fill, shape }) } } #[derive( Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, Parse, Serialize, SpecifiedValueInfo, ToCss, ToComputedValue, ToResolvedValue, ToShmem, )] #[repr(C)] #[css(bitflags( single = "auto", mixed = "over,under,left,right", validate_mixed = "Self::validate_and_simplify" ))] /// Values for text-emphasis-position: /// pub struct TextEmphasisPosition(u8); bitflags! { impl TextEmphasisPosition: u8 { /// Automatically choose mark position based on language. const AUTO = 1 << 0; /// Draw marks over the text in horizontal writing mode. const OVER = 1 << 1; /// Draw marks under the text in horizontal writing mode. const UNDER = 1 << 2; /// Draw marks to the left of the text in vertical writing mode. const LEFT = 1 << 3; /// Draw marks to the right of the text in vertical writing mode. const RIGHT = 1 << 4; } } impl TextEmphasisPosition { fn validate_and_simplify(&mut self) -> bool { // Require one but not both of 'over' and 'under'. if self.intersects(Self::OVER) == self.intersects(Self::UNDER) { return false; } // If 'left' is present, 'right' must be absent. if self.intersects(Self::LEFT) { return !self.intersects(Self::RIGHT); } self.remove(Self::RIGHT); // Right is the default true } } /// Values for the `word-break` property. #[repr(u8)] #[derive( Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] #[allow(missing_docs)] pub enum WordBreak { Normal, BreakAll, KeepAll, /// The break-word value, needed for compat. /// /// Specifying `word-break: break-word` makes `overflow-wrap` behave as /// `anywhere`, and `word-break` behave like `normal`. #[cfg(feature = "gecko")] BreakWord, } /// Values for the `text-justify` CSS property. #[repr(u8)] #[derive( Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] #[allow(missing_docs)] pub enum TextJustify { Auto, None, InterWord, // See https://drafts.csswg.org/css-text-3/#valdef-text-justify-distribute // and https://github.com/w3c/csswg-drafts/issues/6156 for the alias. #[parse(aliases = "distribute")] InterCharacter, } /// Values for the `-moz-control-character-visibility` CSS property. #[repr(u8)] #[derive( Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] #[allow(missing_docs)] pub enum MozControlCharacterVisibility { Hidden, Visible, } #[cfg(feature = "gecko")] impl Default for MozControlCharacterVisibility { fn default() -> Self { if static_prefs::pref!("layout.css.control-characters.visible") { Self::Visible } else { Self::Hidden } } } /// Values for the `line-break` property. #[repr(u8)] #[derive( Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] #[allow(missing_docs)] pub enum LineBreak { Auto, Loose, Normal, Strict, Anywhere, } /// Values for the `overflow-wrap` property. #[repr(u8)] #[derive( Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] #[allow(missing_docs)] pub enum OverflowWrap { Normal, BreakWord, Anywhere, } /// A specified value for the `text-indent` property /// which takes the grammar of [] && hanging? && each-line? /// /// https://drafts.csswg.org/css-text/#propdef-text-indent pub type TextIndent = GenericTextIndent; impl Parse for TextIndent { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let mut length = None; let mut hanging = false; let mut each_line = false; // The length-percentage and the two possible keywords can occur in any order. while !input.is_exhausted() { // If we haven't seen a length yet, try to parse one. if length.is_none() { if let Ok(len) = input .try_parse(|i| LengthPercentage::parse_quirky(context, i, AllowQuirks::Yes)) { length = Some(len); continue; } } if static_prefs::pref!("layout.css.text-indent-keywords.enabled") { // Check for the keywords (boolean flags). try_match_ident_ignore_ascii_case! { input, "hanging" if !hanging => hanging = true, "each-line" if !each_line => each_line = true, } continue; } // If we reach here, there must be something that we failed to parse; // just break and let the caller deal with it. break; } // The length-percentage value is required for the declaration to be valid. if let Some(length) = length { Ok(Self { length, hanging, each_line, }) } else { Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) } } } /// Implements text-decoration-skip-ink which takes the keywords auto | none | all /// /// https://drafts.csswg.org/css-text-decor-4/#text-decoration-skip-ink-property #[repr(u8)] #[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] #[derive( Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] #[allow(missing_docs)] pub enum TextDecorationSkipInk { Auto, None, All, } /// Implements type for `text-decoration-thickness` property pub type TextDecorationLength = GenericTextDecorationLength; impl TextDecorationLength { /// `Auto` value. #[inline] pub fn auto() -> Self { GenericTextDecorationLength::Auto } /// Whether this is the `Auto` value. #[inline] pub fn is_auto(&self) -> bool { matches!(*self, GenericTextDecorationLength::Auto) } } #[derive( Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem, )] #[css(bitflags( single = "auto", mixed = "from-font,under,left,right", validate_mixed = "Self::validate_mixed_flags", ))] #[repr(C)] /// Specified keyword values for the text-underline-position property. /// (Non-exclusive, but not all combinations are allowed: the spec grammar gives /// `auto | [ from-font | under ] || [ left | right ]`.) /// https://drafts.csswg.org/css-text-decor-4/#text-underline-position-property pub struct TextUnderlinePosition(u8); bitflags! { impl TextUnderlinePosition: u8 { /// Use automatic positioning below the alphabetic baseline. const AUTO = 0; /// Use underline position from the first available font. const FROM_FONT = 1 << 0; /// Below the glyph box. const UNDER = 1 << 1; /// In vertical mode, place to the left of the text. const LEFT = 1 << 2; /// In vertical mode, place to the right of the text. const RIGHT = 1 << 3; } } impl TextUnderlinePosition { fn validate_mixed_flags(&self) -> bool { if self.contains(Self::LEFT | Self::RIGHT) { // left and right can't be mixed together. return false; } if self.contains(Self::FROM_FONT | Self::UNDER) { // from-font and under can't be mixed together either. return false; } true } } impl ToCss for TextUnderlinePosition { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: Write, { if self.is_empty() { return dest.write_str("auto"); } let mut writer = SequenceWriter::new(dest, " "); let mut any = false; macro_rules! maybe_write { ($ident:ident => $str:expr) => { if self.contains(TextUnderlinePosition::$ident) { any = true; writer.raw_item($str)?; } }; } maybe_write!(FROM_FONT => "from-font"); maybe_write!(UNDER => "under"); maybe_write!(LEFT => "left"); maybe_write!(RIGHT => "right"); debug_assert!(any); Ok(()) } } /// Values for `ruby-position` property #[repr(u8)] #[derive( Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem, )] #[allow(missing_docs)] pub enum RubyPosition { AlternateOver, AlternateUnder, Over, Under, } impl Parse for RubyPosition { fn parse<'i, 't>( _context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { // Parse alternate before let alternate = input .try_parse(|i| i.expect_ident_matching("alternate")) .is_ok(); if alternate && input.is_exhausted() { return Ok(RubyPosition::AlternateOver); } // Parse over / under let over = try_match_ident_ignore_ascii_case! { input, "over" => true, "under" => false, }; // Parse alternate after let alternate = alternate || input .try_parse(|i| i.expect_ident_matching("alternate")) .is_ok(); Ok(match (over, alternate) { (true, true) => RubyPosition::AlternateOver, (false, true) => RubyPosition::AlternateUnder, (true, false) => RubyPosition::Over, (false, false) => RubyPosition::Under, }) } } impl ToCss for RubyPosition { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: Write, { dest.write_str(match self { RubyPosition::AlternateOver => "alternate", RubyPosition::AlternateUnder => "alternate under", RubyPosition::Over => "over", RubyPosition::Under => "under", }) } } impl SpecifiedValueInfo for RubyPosition { fn collect_completion_keywords(f: KeywordsCollectFn) { f(&["alternate", "over", "under"]) } }