/* 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::LineHeight as ComputedLineHeight; use crate::values::computed::text::TextEmphasisKeywordValue as ComputedTextEmphasisKeywordValue; 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::LineHeight as GenericLineHeight; use crate::values::generics::text::Spacing; use crate::values::specified::length::NonNegativeLengthPercentage; use crate::values::specified::length::{FontRelativeLength, Length}; use crate::values::specified::length::{LengthPercentage, NoCalcLength}; use crate::values::specified::{AllowQuirks, Integer, NonNegativeNumber, Number}; use cssparser::{Parser, Token}; use selectors::parser::SelectorParseErrorKind; use std::fmt::{self, Write}; use style_traits::values::SequenceWriter; use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; use unicode_segmentation::UnicodeSegmentation; /// 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 specified value for the `line-height` property. pub type LineHeight = GenericLineHeight; impl Parse for InitialLetter { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { if input.try(|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(|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) }) } } impl ToComputedValue for LineHeight { type ComputedValue = ComputedLineHeight; #[inline] fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { use crate::values::computed::Length as ComputedLength; use crate::values::specified::length::FontBaseSize; match *self { GenericLineHeight::Normal => GenericLineHeight::Normal, #[cfg(feature = "gecko")] GenericLineHeight::MozBlockHeight => GenericLineHeight::MozBlockHeight, GenericLineHeight::Number(number) => { GenericLineHeight::Number(number.to_computed_value(context)) }, GenericLineHeight::Length(ref non_negative_lp) => { let result = match non_negative_lp.0 { LengthPercentage::Length(NoCalcLength::Absolute(ref abs)) => { context .maybe_zoom_text(abs.to_computed_value(context).into()) .0 }, LengthPercentage::Length(ref length) => length.to_computed_value(context), LengthPercentage::Percentage(ref p) => FontRelativeLength::Em(p.0) .to_computed_value(context, FontBaseSize::CurrentStyle), LengthPercentage::Calc(ref calc) => { let computed_calc = calc.to_computed_value_zoomed(context, FontBaseSize::CurrentStyle); let font_relative_length = FontRelativeLength::Em(computed_calc.percentage()) .to_computed_value(context, FontBaseSize::CurrentStyle) .px(); let absolute_length = computed_calc.unclamped_length().px(); let pixel = computed_calc .clamping_mode .clamp(absolute_length + font_relative_length); ComputedLength::new(pixel) }, }; GenericLineHeight::Length(result.into()) }, } } #[inline] fn from_computed_value(computed: &Self::ComputedValue) -> Self { match *computed { GenericLineHeight::Normal => GenericLineHeight::Normal, #[cfg(feature = "gecko")] GenericLineHeight::MozBlockHeight => GenericLineHeight::MozBlockHeight, GenericLineHeight::Number(ref number) => { GenericLineHeight::Number(NonNegativeNumber::from_computed_value(number)) }, GenericLineHeight::Length(ref length) => { GenericLineHeight::Length(NoCalcLength::from_computed_value(&length.0).into()) }, } } } /// A generic value for the `text-overflow` property. #[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] 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(Box), } impl Parse for TextOverflowSide { fn parse<'i, 't>( _context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let location = input.current_source_location(); match *input.next()? { Token::Ident(ref ident) => { match_ignore_ascii_case! { ident, "clip" => Ok(TextOverflowSide::Clip), "ellipsis" => Ok(TextOverflowSide::Ellipsis), _ => Err(location.new_custom_error( SelectorParseErrorKind::UnexpectedIdent(ident.clone()) )) } }, Token::QuotedString(ref v) => Ok(TextOverflowSide::String( v.as_ref().to_owned().into_boxed_str(), )), ref t => Err(location.new_unexpected_token_error(t.clone())), } } } #[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(|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()), } } } } bitflags! { #[derive(MallocSizeOf, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem)] #[value_info(other_values = "none,underline,overline,line-through,blink")] #[repr(C)] /// Specified keyword values for the text-decoration-line property. pub struct 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 Parse for TextDecorationLine { /// none | [ underline || overline || line-through || blink ] fn parse<'i, 't>( _context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let mut result = TextDecorationLine::empty(); // NOTE(emilio): this loop has this weird structure because we run this // code to parse the text-decoration shorthand as well, so we need to // ensure we don't return an error if we don't consume the whole thing // because we find an invalid identifier or other kind of token. loop { let flag: Result<_, ParseError<'i>> = input.try(|input| { let flag = try_match_ident_ignore_ascii_case! { input, "none" if result.is_empty() => TextDecorationLine::NONE, "underline" => TextDecorationLine::UNDERLINE, "overline" => TextDecorationLine::OVERLINE, "line-through" => TextDecorationLine::LINE_THROUGH, "blink" => TextDecorationLine::BLINK, }; Ok(flag) }); let flag = match flag { Ok(flag) => flag, Err(..) => break, }; if flag.is_empty() { return Ok(TextDecorationLine::NONE); } if result.contains(flag) { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } result.insert(flag) } if !result.is_empty() { Ok(result) } else { Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) } } } impl ToCss for TextDecorationLine { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: Write, { if self.is_empty() { return dest.write_str("none"); } #[cfg(feature = "gecko")] { if *self == TextDecorationLine::COLOR_OVERRIDE { return Ok(()); } } let mut writer = SequenceWriter::new(dest, " "); let mut any = false; macro_rules! maybe_write { ($ident:ident => $str:expr) => { if self.contains(TextDecorationLine::$ident) { any = true; writer.raw_item($str)?; } }; } maybe_write!(UNDERLINE => "underline"); maybe_write!(OVERLINE => "overline"); maybe_write!(LINE_THROUGH => "line-through"); maybe_write!(BLINK => "blink"); debug_assert!(any); Ok(()) } } impl TextDecorationLine { #[inline] /// Returns the initial value of text-decoration-line pub fn none() -> Self { TextDecorationLine::NONE } } /// 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)] pub enum TextAlignKeyword { Start, End, Left, Right, Center, Justify, #[cfg(feature = "gecko")] MozCenter, #[cfg(feature = "gecko")] MozLeft, #[cfg(feature = "gecko")] MozRight, #[cfg(feature = "servo")] ServoCenter, #[cfg(feature = "servo")] ServoLeft, #[cfg(feature = "servo")] ServoRight, #[css(skip)] #[cfg(feature = "gecko")] Char, } /// Specified value of text-align property. #[cfg_attr(feature = "gecko", derive(MallocSizeOf))] #[derive(Clone, Copy, Debug, Eq, Hash, 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, /// `MozCenterOrInherit` value of text-align property. It cannot be parsed, /// only set directly on the elements and it has a different handling /// unlike other values. #[cfg(feature = "gecko")] #[css(skip)] MozCenterOrInherit, } impl TextAlign { /// Convert an enumerated value coming from Gecko to a `TextAlign`. #[cfg(feature = "gecko")] pub fn from_gecko_keyword(kw: u32) -> Self { TextAlign::Keyword(TextAlignKeyword::from_gecko_keyword(kw)) } } 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.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) } } /// Specified value of text-emphasis-style property. #[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] pub enum TextEmphasisStyle { /// Keyword(TextEmphasisKeywordValue), /// `none` None, /// String (will be used only first grapheme cluster) for the text-emphasis-style property String(String), } /// Keyword value for the text-emphasis-style property #[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] pub enum TextEmphasisKeywordValue { /// Fill(TextEmphasisFillMode), /// Shape(TextEmphasisShapeKeyword), /// FillAndShape(TextEmphasisFillMode, TextEmphasisShapeKeyword), } impl TextEmphasisKeywordValue { fn fill(&self) -> Option { match *self { TextEmphasisKeywordValue::Fill(fill) | TextEmphasisKeywordValue::FillAndShape(fill, _) => Some(fill), _ => None, } } fn shape(&self) -> Option { match *self { TextEmphasisKeywordValue::Shape(shape) | TextEmphasisKeywordValue::FillAndShape(_, shape) => Some(shape), _ => None, } } } /// Fill mode for the text-emphasis-style property #[derive(Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] pub enum TextEmphasisFillMode { /// `filled` Filled, /// `open` Open, } /// Shape keyword for the text-emphasis-style property #[derive( Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, )] pub enum TextEmphasisShapeKeyword { /// `dot` Dot, /// `circle` Circle, /// `double-circle` DoubleCircle, /// `triangle` Triangle, /// `sesame` Sesame, } impl TextEmphasisShapeKeyword { /// converts fill mode to a unicode char pub fn char(&self, fill: TextEmphasisFillMode) -> &str { let fill = fill == TextEmphasisFillMode::Filled; match *self { TextEmphasisShapeKeyword::Dot => { if fill { "\u{2022}" } else { "\u{25e6}" } }, TextEmphasisShapeKeyword::Circle => { if fill { "\u{25cf}" } else { "\u{25cb}" } }, TextEmphasisShapeKeyword::DoubleCircle => { if fill { "\u{25c9}" } else { "\u{25ce}" } }, TextEmphasisShapeKeyword::Triangle => { if fill { "\u{25b2}" } else { "\u{25b3}" } }, TextEmphasisShapeKeyword::Sesame => { if fill { "\u{fe45}" } else { "\u{fe46}" } }, } } } impl ToComputedValue for TextEmphasisStyle { type ComputedValue = ComputedTextEmphasisStyle; #[inline] fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { match *self { TextEmphasisStyle::Keyword(ref keyword) => { // FIXME(emilio): This should set the rule_cache_conditions // properly. let default_shape = if context.style().get_inherited_box().clone_writing_mode() == SpecifiedWritingMode::HorizontalTb { TextEmphasisShapeKeyword::Circle } else { TextEmphasisShapeKeyword::Sesame }; ComputedTextEmphasisStyle::Keyword(ComputedTextEmphasisKeywordValue { fill: keyword.fill().unwrap_or(TextEmphasisFillMode::Filled), shape: keyword.shape().unwrap_or(default_shape), }) }, TextEmphasisStyle::None => ComputedTextEmphasisStyle::None, TextEmphasisStyle::String(ref s) => { // Passing `true` to iterate over extended grapheme clusters, following // recommendation at http://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries let string = s.graphemes(true).next().unwrap_or("").to_string(); ComputedTextEmphasisStyle::String(string) }, } } #[inline] fn from_computed_value(computed: &Self::ComputedValue) -> Self { match *computed { ComputedTextEmphasisStyle::Keyword(ref keyword) => TextEmphasisStyle::Keyword( TextEmphasisKeywordValue::FillAndShape(keyword.fill, keyword.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(|input| input.expect_ident_matching("none")) .is_ok() { return Ok(TextEmphasisStyle::None); } if let Ok(s) = input.try(|i| i.expect_string().map(|s| s.as_ref().to_owned())) { // Handle return Ok(TextEmphasisStyle::String(s)); } // Handle a pair of keywords let mut shape = input.try(TextEmphasisShapeKeyword::parse).ok(); let fill = input.try(TextEmphasisFillMode::parse).ok(); if shape.is_none() { shape = input.try(TextEmphasisShapeKeyword::parse).ok(); } // At least one of shape or fill must be handled let keyword_value = match (fill, shape) { (Some(fill), Some(shape)) => TextEmphasisKeywordValue::FillAndShape(fill, shape), (Some(fill), None) => TextEmphasisKeywordValue::Fill(fill), (None, Some(shape)) => TextEmphasisKeywordValue::Shape(shape), _ => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), }; Ok(TextEmphasisStyle::Keyword(keyword_value)) } } /// The allowed horizontal values for the `text-emphasis-position` property. #[derive( Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] pub enum TextEmphasisHorizontalWritingModeValue { /// Draw marks over the text in horizontal writing mode. Over, /// Draw marks under the text in horizontal writing mode. Under, } /// The allowed vertical values for the `text-emphasis-position` property. #[derive( Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] pub enum TextEmphasisVerticalWritingModeValue { /// Draws marks to the right of the text in vertical writing mode. Right, /// Draw marks to the left of the text in vertical writing mode. Left, } /// Specified value of `text-emphasis-position` property. #[derive( Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] pub struct TextEmphasisPosition( pub TextEmphasisHorizontalWritingModeValue, pub TextEmphasisVerticalWritingModeValue, ); impl TextEmphasisPosition { #[inline] /// Returns the initial value of `text-emphasis-position` pub fn over_right() -> Self { TextEmphasisPosition( TextEmphasisHorizontalWritingModeValue::Over, TextEmphasisVerticalWritingModeValue::Right, ) } #[cfg(feature = "gecko")] /// Converts an enumerated value coming from Gecko to a `TextEmphasisPosition`. pub fn from_gecko_keyword(kw: u32) -> Self { use crate::gecko_bindings::structs; let vert = if kw & structs::NS_STYLE_TEXT_EMPHASIS_POSITION_RIGHT != 0 { TextEmphasisVerticalWritingModeValue::Right } else { debug_assert!(kw & structs::NS_STYLE_TEXT_EMPHASIS_POSITION_LEFT != 0); TextEmphasisVerticalWritingModeValue::Left }; let horiz = if kw & structs::NS_STYLE_TEXT_EMPHASIS_POSITION_OVER != 0 { TextEmphasisHorizontalWritingModeValue::Over } else { debug_assert!(kw & structs::NS_STYLE_TEXT_EMPHASIS_POSITION_UNDER != 0); TextEmphasisHorizontalWritingModeValue::Under }; TextEmphasisPosition(horiz, vert) } } impl Parse for TextEmphasisPosition { fn parse<'i, 't>( _context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { if let Ok(horizontal) = input.try(|input| TextEmphasisHorizontalWritingModeValue::parse(input)) { let vertical = TextEmphasisVerticalWritingModeValue::parse(input)?; Ok(TextEmphasisPosition(horizontal, vertical)) } else { let vertical = TextEmphasisVerticalWritingModeValue::parse(input)?; let horizontal = TextEmphasisHorizontalWritingModeValue::parse(input)?; Ok(TextEmphasisPosition(horizontal, vertical)) } } } #[cfg(feature = "gecko")] impl From for TextEmphasisPosition { fn from(bits: u8) -> Self { TextEmphasisPosition::from_gecko_keyword(bits as u32) } } #[cfg(feature = "gecko")] impl From for u8 { fn from(v: TextEmphasisPosition) -> u8 { use crate::gecko_bindings::structs; let mut result = match v.0 { TextEmphasisHorizontalWritingModeValue::Over => { structs::NS_STYLE_TEXT_EMPHASIS_POSITION_OVER }, TextEmphasisHorizontalWritingModeValue::Under => { structs::NS_STYLE_TEXT_EMPHASIS_POSITION_UNDER }, }; match v.1 { TextEmphasisVerticalWritingModeValue::Right => { result |= structs::NS_STYLE_TEXT_EMPHASIS_POSITION_RIGHT; }, TextEmphasisVerticalWritingModeValue::Left => { result |= structs::NS_STYLE_TEXT_EMPHASIS_POSITION_LEFT; }, }; result as u8 } } /// 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 `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, }