/* 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::text::TextOverflow as ComputedTextOverflow; use crate::values::computed::{Context, ToComputedValue}; use crate::values::generics::text::InitialLetter as GenericInitialLetter; use crate::values::generics::text::{GenericTextDecorationLength, GenericTextIndent, Spacing}; use crate::values::specified::length::{Length, LengthPercentage}; use crate::values::specified::{AllowQuirks, Integer, Number}; use cssparser::{Parser, Token}; use icu_segmenter::GraphemeClusterSegmenter; use selectors::parser::SelectorParseErrorKind; 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(GenericInitialLetter::Normal); } let size = Number::parse_at_least_one(context, input)?; let sink = input .try_parse(|i| Integer::parse_positive(context, i)) .ok(); Ok(GenericInitialLetter::Specified(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::OwnedStr), } #[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] /// text-overflow. Specifies rendering when inline content overflows its line box edge. pub struct TextOverflow { /// First value. Applies to end line box edge if no second is supplied; line-left edge otherwise. pub first: TextOverflowSide, /// Second value. Applies to the line-right edge if supplied. pub second: Option, } impl Parse for TextOverflow { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let first = TextOverflowSide::parse(context, input)?; let second = input .try_parse(|input| TextOverflowSide::parse(context, input)) .ok(); Ok(TextOverflow { first, second }) } } impl ToComputedValue for TextOverflow { type ComputedValue = ComputedTextOverflow; #[inline] fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue { if let Some(ref second) = self.second { Self::ComputedValue { first: self.first.clone(), second: second.clone(), sides_are_logical: false, } } else { Self::ComputedValue { first: TextOverflowSide::Clip, second: self.first.clone(), sides_are_logical: true, } } } #[inline] fn from_computed_value(computed: &Self::ComputedValue) -> Self { if computed.sides_are_logical { assert_eq!(computed.first, TextOverflowSide::Clip); TextOverflow { first: computed.second.clone(), second: None, } } else { TextOverflow { first: computed.first.clone(), second: Some(computed.second.clone()), } } } } #[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, ToResolvedValue, ToShmem, )] #[repr(C)] /// Specified value of the text-transform property, stored in two parts: /// the case-related transforms (mutually exclusive, only one may be in effect), and others (non-exclusive). pub struct TextTransform { /// Case transform, if any. pub case_: TextTransformCase, /// Non-case transforms. pub other_: TextTransformOther, } impl TextTransform { #[inline] /// Returns the initial value of text-transform pub fn none() -> Self { TextTransform { case_: TextTransformCase::None, other_: TextTransformOther::empty(), } } #[inline] /// Returns whether the value is 'none' pub fn is_none(&self) -> bool { self.case_ == TextTransformCase::None && self.other_.is_empty() } } // TODO: This can be simplified by deriving it. impl Parse for TextTransform { fn parse<'i, 't>( _context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let mut result = TextTransform::none(); // Case keywords are mutually exclusive; other transforms may co-occur. loop { let location = input.current_source_location(); let ident = match input.next() { Ok(&Token::Ident(ref ident)) => ident, Ok(other) => return Err(location.new_unexpected_token_error(other.clone())), Err(..) => break, }; match_ignore_ascii_case! { ident, "none" if result.is_none() => { return Ok(result); }, "uppercase" if result.case_ == TextTransformCase::None => { result.case_ = TextTransformCase::Uppercase }, "lowercase" if result.case_ == TextTransformCase::None => { result.case_ = TextTransformCase::Lowercase }, "capitalize" if result.case_ == TextTransformCase::None => { result.case_ = TextTransformCase::Capitalize }, "math-auto" if result.case_ == TextTransformCase::None && result.other_.is_empty() => { result.case_ = TextTransformCase::MathAuto; return Ok(result); }, "full-width" if !result.other_.intersects(TextTransformOther::FULL_WIDTH) => { result.other_.insert(TextTransformOther::FULL_WIDTH) }, "full-size-kana" if !result.other_.intersects(TextTransformOther::FULL_SIZE_KANA) => { result.other_.insert(TextTransformOther::FULL_SIZE_KANA) }, _ => return Err(location.new_custom_error( SelectorParseErrorKind::UnexpectedIdent(ident.clone()) )), } } if result.is_none() { Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) } else { Ok(result) } } } impl ToCss for TextTransform { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: Write, { if self.is_none() { return dest.write_str("none"); } if self.case_ != TextTransformCase::None { self.case_.to_css(dest)?; if !self.other_.is_empty() { dest.write_char(' ')?; } } self.other_.to_css(dest) } } #[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. MathAuto, } #[derive( Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, Parse, Serialize, SpecifiedValueInfo, ToCss, ToComputedValue, ToResolvedValue, ToShmem, )] #[css(bitflags(mixed = "full-width,full-size-kana"))] #[repr(C)] /// Specified keyword values for non-case transforms in the text-transform property. (Non-exclusive.) pub struct TextTransformOther(u8); bitflags! { impl TextTransformOther: u8 { /// full-width const FULL_WIDTH = 1 << 0; /// full-size-kana const FULL_SIZE_KANA = 1 << 1; } } /// 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, #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))] Justify, #[css(skip)] #[cfg(feature = "gecko")] Char, End, #[cfg(feature = "gecko")] MozCenter, #[cfg(feature = "gecko")] MozLeft, #[cfg(feature = "gecko")] MozRight, #[cfg(feature = "servo-layout-2013")] ServoCenter, #[cfg(feature = "servo-layout-2013")] ServoLeft, #[cfg(feature = "servo-layout-2013")] ServoRight, } /// 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. #[cfg(feature = "gecko")] #[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, } }, #[cfg(feature = "gecko")] 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( mixed = "over,under,left,right", validate_mixed = "Self::validate_and_simplify" ))] /// Values for text-emphasis-position: /// pub struct TextEmphasisPosition(u8); bitflags! { impl TextEmphasisPosition: u8 { /// Draws marks to the right of the text in vertical writing mode. const OVER = 1 << 0; /// Draw marks under the text in horizontal writing mode. const UNDER = 1 << 1; /// Draw marks to the left of the text in vertical writing mode. const LEFT = 1 << 2; /// Draws marks to the right of the text in vertical writing mode. const RIGHT = 1 << 3; } } impl TextEmphasisPosition { fn validate_and_simplify(&mut self) -> bool { if self.intersects(Self::OVER) == self.intersects(Self::UNDER) { return false; } 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, } 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, PartialEq, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem, )] #[value_info(other_values = "auto,from-font,under,left,right")] #[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; } } // TODO: This can be derived with some care. impl Parse for TextUnderlinePosition { fn parse<'i, 't>( _context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let mut result = TextUnderlinePosition::empty(); loop { let location = input.current_source_location(); let ident = match input.next() { Ok(&Token::Ident(ref ident)) => ident, Ok(other) => return Err(location.new_unexpected_token_error(other.clone())), Err(..) => break, }; match_ignore_ascii_case! { ident, "auto" if result.is_empty() => { return Ok(result); }, "from-font" if !result.intersects(TextUnderlinePosition::FROM_FONT | TextUnderlinePosition::UNDER) => { result.insert(TextUnderlinePosition::FROM_FONT); }, "under" if !result.intersects(TextUnderlinePosition::FROM_FONT | TextUnderlinePosition::UNDER) => { result.insert(TextUnderlinePosition::UNDER); }, "left" if !result.intersects(TextUnderlinePosition::LEFT | TextUnderlinePosition::RIGHT) => { result.insert(TextUnderlinePosition::LEFT); }, "right" if !result.intersects(TextUnderlinePosition::LEFT | TextUnderlinePosition::RIGHT) => { result.insert(TextUnderlinePosition::RIGHT); }, _ => return Err(location.new_custom_error( SelectorParseErrorKind::UnexpectedIdent(ident.clone()) )), } } if !result.is_empty() { Ok(result) } else { Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) } } } 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"]) } }