diff options
Diffstat (limited to '')
-rw-r--r-- | servo/components/style/values/specified/text.rs | 1129 |
1 files changed, 1129 insertions, 0 deletions
diff --git a/servo/components/style/values/specified/text.rs b/servo/components/style/values/specified/text.rs new file mode 100644 index 0000000000..60eedb7872 --- /dev/null +++ b/servo/components/style/values/specified/text.rs @@ -0,0 +1,1129 @@ +/* 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::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::{GenericTextDecorationLength, 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 style_traits::{KeywordsCollectFn, SpecifiedValueInfo}; +use unicode_segmentation::UnicodeSegmentation; + +/// A specified type for the `initial-letter` property. +pub type InitialLetter = GenericInitialLetter<Number, Integer>; + +/// A specified value for the `letter-spacing` property. +pub type LetterSpacing = Spacing<Length>; + +/// A specified value for the `word-spacing` property. +pub type WordSpacing = Spacing<LengthPercentage>; + +/// A specified value for the `line-height` property. +pub type LineHeight = GenericLineHeight<NonNegativeNumber, NonNegativeLengthPercentage>; + +/// 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>` + String(crate::OwnedStr), +} + +impl Parse for InitialLetter { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + 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<Self, ParseError<'i>> { + 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<Self, ParseError<'i>> { + 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::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)) + }, + 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 base = context.style().get_font().clone_font_size().computed_size(); + computed_calc.resolve(base) + }, + }; + 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, + 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<TextOverflowSide>, +} + +impl Parse for TextOverflow { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<TextOverflow, ParseError<'i>> { + 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()), + } + } + } +} + +bitflags! { + #[derive(Clone, Copy, 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 { + /// 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 <a href=foo><font color="red">text</font></a> + /// 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<Self, ParseError<'i>> { + 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 + }, + "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<W>(&self, dest: &mut CssWriter<W>) -> 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, +} + +bitflags! { + #[derive(Clone, Copy, 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 { + /// 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::in_ua_or_chrome_sheet")] + 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 <html> element we should still respect the dir + // but the parent dir of that element is LTR even if it's <html dir=rtl> + // 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<TextEmphasisShapeKeyword>, +) -> 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 { + /// [ <fill> || <shape> ] + Keyword { + #[css(contextual_skip_if = "fill_mode_is_default_and_shape_exists")] + fill: TextEmphasisFillMode, + shape: Option<TextEmphasisShapeKeyword>, + }, + /// `none` + None, + /// `<string>` (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) => { + // Passing `true` to iterate over extended grapheme clusters, following + // recommendation at http://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries + // + // 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(). + let s = s.graphemes(true).next().unwrap_or("").to_string(); + ComputedTextEmphasisStyle::String(s.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<Self, ParseError<'i>> { + 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 <string> + 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 `<shape>` depends on the + // computed writing-mode. + Ok(TextEmphasisStyle::Keyword { fill, shape }) + } +} + +bitflags! { + #[derive(Clone, Copy, 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: + /// <https://drafts.csswg.org/css-text-decor/#text-emphasis-position-property> + pub struct 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, +} + +/// 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<LengthPercentage>; + +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) + } +} + +bitflags! { + #[derive(Clone, Copy, 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 { + /// 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<TextUnderlinePosition, ParseError<'i>> { + 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<W>(&self, dest: &mut CssWriter<W>) -> 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<RubyPosition, ParseError<'i>> { + // 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<W>(&self, dest: &mut CssWriter<W>) -> 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"]) + } +} |