| //! CSS properties related to text. |
| |
| #![allow(non_upper_case_globals)] |
| |
| use super::{Property, PropertyId}; |
| use crate::compat; |
| use crate::context::PropertyHandlerContext; |
| use crate::declaration::{DeclarationBlock, DeclarationList}; |
| use crate::error::{ParserError, PrinterError}; |
| use crate::macros::{define_shorthand, enum_property}; |
| use crate::prefixes::Feature; |
| use crate::printer::Printer; |
| use crate::targets::{should_compile, Browsers, Targets}; |
| use crate::traits::{FallbackValues, IsCompatible, Parse, PropertyHandler, Shorthand, ToCss, Zero}; |
| use crate::values::calc::{Calc, MathFunction}; |
| use crate::values::color::{ColorFallbackKind, CssColor}; |
| use crate::values::length::{Length, LengthPercentage, LengthValue}; |
| use crate::values::percentage::Percentage; |
| use crate::values::string::CSSString; |
| use crate::vendor_prefix::VendorPrefix; |
| #[cfg(feature = "visitor")] |
| use crate::visitor::Visit; |
| use bitflags::bitflags; |
| use cssparser::*; |
| use smallvec::SmallVec; |
| |
| enum_property! { |
| /// Defines how text case should be transformed in the |
| /// [text-transform](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-transform-property) property. |
| pub enum TextTransformCase { |
| /// Text should not be transformed. |
| None, |
| /// Text should be uppercased. |
| Uppercase, |
| /// Text should be lowercased. |
| Lowercase, |
| /// Each word should be capitalized. |
| Capitalize, |
| } |
| } |
| |
| impl Default for TextTransformCase { |
| fn default() -> TextTransformCase { |
| TextTransformCase::None |
| } |
| } |
| |
| bitflags! { |
| /// Defines how ideographic characters should be transformed in the |
| /// [text-transform](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-transform-property) property. |
| /// |
| /// All combinations of flags is supported. |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(from = "SerializedTextTransformOther", into = "SerializedTextTransformOther"))] |
| #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)] |
| pub struct TextTransformOther: u8 { |
| /// Puts all typographic character units in full-width form. |
| const FullWidth = 0b00000001; |
| /// Converts all small Kana characters to the equivalent full-size Kana. |
| const FullSizeKana = 0b00000010; |
| } |
| } |
| |
| impl<'i> Parse<'i> for TextTransformOther { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| let location = input.current_source_location(); |
| let ident = input.expect_ident()?; |
| match_ignore_ascii_case! { &ident, |
| "full-width" => Ok(TextTransformOther::FullWidth), |
| "full-size-kana" => Ok(TextTransformOther::FullSizeKana), |
| _ => Err(location.new_unexpected_token_error( |
| cssparser::Token::Ident(ident.clone()) |
| )) |
| } |
| } |
| } |
| |
| impl ToCss for TextTransformOther { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| let mut needs_space = false; |
| if self.contains(TextTransformOther::FullWidth) { |
| dest.write_str("full-width")?; |
| needs_space = true; |
| } |
| |
| if self.contains(TextTransformOther::FullSizeKana) { |
| if needs_space { |
| dest.write_char(' ')?; |
| } |
| dest.write_str("full-size-kana")?; |
| } |
| |
| Ok(()) |
| } |
| } |
| |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(rename_all = "camelCase") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| struct SerializedTextTransformOther { |
| /// Puts all typographic character units in full-width form. |
| full_width: bool, |
| /// Converts all small Kana characters to the equivalent full-size Kana. |
| full_size_kana: bool, |
| } |
| |
| impl From<TextTransformOther> for SerializedTextTransformOther { |
| fn from(t: TextTransformOther) -> Self { |
| Self { |
| full_width: t.contains(TextTransformOther::FullWidth), |
| full_size_kana: t.contains(TextTransformOther::FullSizeKana), |
| } |
| } |
| } |
| |
| impl From<SerializedTextTransformOther> for TextTransformOther { |
| fn from(t: SerializedTextTransformOther) -> Self { |
| let mut res = TextTransformOther::empty(); |
| if t.full_width { |
| res |= TextTransformOther::FullWidth; |
| } |
| if t.full_size_kana { |
| res |= TextTransformOther::FullSizeKana; |
| } |
| res |
| } |
| } |
| |
| #[cfg(feature = "jsonschema")] |
| #[cfg_attr(docsrs, doc(cfg(feature = "jsonschema")))] |
| impl<'a> schemars::JsonSchema for TextTransformOther { |
| fn is_referenceable() -> bool { |
| true |
| } |
| |
| fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { |
| SerializedTextTransformOther::json_schema(gen) |
| } |
| |
| fn schema_name() -> String { |
| "TextTransformOther".into() |
| } |
| } |
| |
| /// A value for the [text-transform](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-transform-property) property. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub struct TextTransform { |
| /// How case should be transformed. |
| pub case: TextTransformCase, |
| /// How ideographic characters should be transformed. |
| #[cfg_attr(feature = "serde", serde(flatten))] |
| pub other: TextTransformOther, |
| } |
| |
| impl<'i> Parse<'i> for TextTransform { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| let mut case = None; |
| let mut other = TextTransformOther::empty(); |
| |
| loop { |
| if case.is_none() { |
| if let Ok(c) = input.try_parse(TextTransformCase::parse) { |
| case = Some(c); |
| if c == TextTransformCase::None { |
| other = TextTransformOther::empty(); |
| break; |
| } |
| continue; |
| } |
| } |
| |
| if let Ok(o) = input.try_parse(TextTransformOther::parse) { |
| other |= o; |
| continue; |
| } |
| |
| break; |
| } |
| |
| Ok(TextTransform { |
| case: case.unwrap_or_default(), |
| other, |
| }) |
| } |
| } |
| |
| impl ToCss for TextTransform { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| let mut needs_space = false; |
| if self.case != TextTransformCase::None || self.other.is_empty() { |
| self.case.to_css(dest)?; |
| needs_space = true; |
| } |
| |
| if !self.other.is_empty() { |
| if needs_space { |
| dest.write_char(' ')?; |
| } |
| self.other.to_css(dest)?; |
| } |
| Ok(()) |
| } |
| } |
| |
| enum_property! { |
| /// A value for the [white-space](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#white-space-property) property. |
| pub enum WhiteSpace { |
| /// Sequences of white space are collapsed into a single character. |
| "normal": Normal, |
| /// White space is not collapsed. |
| "pre": Pre, |
| /// White space is collapsed, but no line wrapping occurs. |
| "nowrap": NoWrap, |
| /// White space is preserved, but line wrapping occurs. |
| "pre-wrap": PreWrap, |
| /// Like pre-wrap, but with different line breaking rules. |
| "break-spaces": BreakSpaces, |
| /// White space is collapsed, but with different line breaking rules. |
| "pre-line": PreLine, |
| } |
| } |
| |
| enum_property! { |
| /// A value for the [word-break](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#word-break-property) property. |
| pub enum WordBreak { |
| /// Words break according to their customary rules. |
| Normal, |
| /// Breaking is forbidden within “words”. |
| KeepAll, |
| /// Breaking is allowed within “words”. |
| BreakAll, |
| /// Breaking is allowed if there is no otherwise acceptable break points in a line. |
| BreakWord, |
| } |
| } |
| |
| enum_property! { |
| /// A value for the [line-break](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#line-break-property) property. |
| pub enum LineBreak { |
| /// The UA determines the set of line-breaking restrictions to use. |
| Auto, |
| /// Breaks text using the least restrictive set of line-breaking rules. |
| Loose, |
| /// Breaks text using the most common set of line-breaking rules. |
| Normal, |
| /// Breaks text using the most stringent set of line-breaking rules. |
| Strict, |
| /// There is a soft wrap opportunity around every typographic character unit. |
| Anywhere, |
| } |
| } |
| enum_property! { |
| /// A value for the [hyphens](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#hyphenation) property. |
| pub enum Hyphens { |
| /// Words are not hyphenated. |
| None, |
| /// Words are only hyphenated where there are characters inside the word that explicitly suggest hyphenation opportunities. |
| Manual, |
| /// Words may be broken at hyphenation opportunities determined automatically by the UA. |
| Auto, |
| } |
| } |
| |
| enum_property! { |
| /// A value for the [overflow-wrap](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#overflow-wrap-property) property. |
| pub enum OverflowWrap { |
| /// Lines may break only at allowed break points. |
| Normal, |
| /// Breaking is allowed if there is no otherwise acceptable break points in a line. |
| Anywhere, |
| /// As for anywhere except that soft wrap opportunities introduced by break-word are |
| /// not considered when calculating min-content intrinsic sizes. |
| BreakWord, |
| } |
| } |
| |
| enum_property! { |
| /// A value for the [text-align](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-align-property) property. |
| pub enum TextAlign { |
| /// Inline-level content is aligned to the start edge of the line box. |
| Start, |
| /// Inline-level content is aligned to the end edge of the line box. |
| End, |
| /// Inline-level content is aligned to the line-left edge of the line box. |
| Left, |
| /// Inline-level content is aligned to the line-right edge of the line box. |
| Right, |
| /// Inline-level content is centered within the line box. |
| Center, |
| /// Text is justified according to the method specified by the text-justify property. |
| Justify, |
| /// Matches the parent element. |
| MatchParent, |
| /// Same as justify, but also justifies the last line. |
| JustifyAll, |
| } |
| } |
| |
| enum_property! { |
| /// A value for the [text-align-last](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-align-last-property) property. |
| pub enum TextAlignLast { |
| /// Content on the affected line is aligned per `text-align-all` unless set to `justify`, in which case it is start-aligned. |
| Auto, |
| /// Inline-level content is aligned to the start edge of the line box. |
| Start, |
| /// Inline-level content is aligned to the end edge of the line box. |
| End, |
| /// Inline-level content is aligned to the line-left edge of the line box. |
| Left, |
| /// Inline-level content is aligned to the line-right edge of the line box. |
| Right, |
| /// Inline-level content is centered within the line box. |
| Center, |
| /// Text is justified according to the method specified by the text-justify property. |
| Justify, |
| /// Matches the parent element. |
| MatchParent, |
| } |
| } |
| |
| enum_property! { |
| /// A value for the [text-justify](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-justify-property) property. |
| pub enum TextJustify { |
| /// The UA determines the justification algorithm to follow. |
| Auto, |
| /// Justification is disabled. |
| None, |
| /// Justification adjusts spacing at word separators only. |
| InterWord, |
| /// Justification adjusts spacing between each character. |
| InterCharacter, |
| } |
| } |
| |
| /// A value for the [word-spacing](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#word-spacing-property) |
| /// and [letter-spacing](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#letter-spacing-property) properties. |
| #[derive(Debug, Clone, PartialEq, Parse, ToCss)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(tag = "type", content = "value", rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub enum Spacing { |
| /// No additional spacing is applied. |
| Normal, |
| /// Additional spacing between each word or letter. |
| Length(Length), |
| } |
| |
| /// A value for the [text-indent](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-indent-property) property. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(rename_all = "camelCase") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub struct TextIndent { |
| /// The amount to indent. |
| pub value: LengthPercentage, |
| /// Inverts which lines are affected. |
| pub hanging: bool, |
| /// Affects the first line after each hard break. |
| pub each_line: bool, |
| } |
| |
| impl<'i> Parse<'i> for TextIndent { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| let mut value = None; |
| let mut hanging = false; |
| let mut each_line = false; |
| |
| loop { |
| if value.is_none() { |
| if let Ok(val) = input.try_parse(LengthPercentage::parse) { |
| value = Some(val); |
| continue; |
| } |
| } |
| |
| if !hanging { |
| if input.try_parse(|input| input.expect_ident_matching("hanging")).is_ok() { |
| hanging = true; |
| continue; |
| } |
| } |
| |
| if !each_line { |
| if input.try_parse(|input| input.expect_ident_matching("each-line")).is_ok() { |
| each_line = true; |
| continue; |
| } |
| } |
| |
| break; |
| } |
| |
| if let Some(value) = value { |
| Ok(TextIndent { |
| value, |
| hanging, |
| each_line, |
| }) |
| } else { |
| Err(input.new_custom_error(ParserError::InvalidDeclaration)) |
| } |
| } |
| } |
| |
| impl ToCss for TextIndent { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| self.value.to_css(dest)?; |
| if self.hanging { |
| dest.write_str(" hanging")?; |
| } |
| if self.each_line { |
| dest.write_str(" each-line")?; |
| } |
| Ok(()) |
| } |
| } |
| |
| /// A value for the [text-size-adjust](https://w3c.github.io/csswg-drafts/css-size-adjust/#adjustment-control) property. |
| #[derive(Debug, Clone, PartialEq, Parse, ToCss)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(tag = "type", content = "value", rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub enum TextSizeAdjust { |
| /// Use the default size adjustment when displaying on a small device. |
| Auto, |
| /// No size adjustment when displaying on a small device. |
| None, |
| /// When displaying on a small device, the font size is multiplied by this percentage. |
| Percentage(Percentage), |
| } |
| |
| bitflags! { |
| /// A value for the [text-decoration-line](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-line-property) property. |
| /// |
| /// Multiple lines may be specified by combining the flags. |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(from = "SerializedTextDecorationLine", into = "SerializedTextDecorationLine"))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)] |
| pub struct TextDecorationLine: u8 { |
| /// Each line of text is underlined. |
| const Underline = 0b00000001; |
| /// Each line of text has a line over it. |
| const Overline = 0b00000010; |
| /// Each line of text has a line through the middle. |
| const LineThrough = 0b00000100; |
| /// The text blinks. |
| const Blink = 0b00001000; |
| /// The text is decorated as a spelling error. |
| const SpellingError = 0b00010000; |
| /// The text is decorated as a grammar error. |
| const GrammarError = 0b00100000; |
| } |
| } |
| |
| impl Default for TextDecorationLine { |
| fn default() -> TextDecorationLine { |
| TextDecorationLine::empty() |
| } |
| } |
| |
| impl<'i> Parse<'i> for TextDecorationLine { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| let mut value = TextDecorationLine::empty(); |
| let mut any = false; |
| |
| loop { |
| let flag: Result<_, ParseError<'i, ParserError<'i>>> = input.try_parse(|input| { |
| let location = input.current_source_location(); |
| let ident = input.expect_ident()?; |
| Ok(match_ignore_ascii_case! { &ident, |
| "none" if value.is_empty() => TextDecorationLine::empty(), |
| "underline" => TextDecorationLine::Underline, |
| "overline" => TextDecorationLine::Overline, |
| "line-through" => TextDecorationLine::LineThrough, |
| "blink" =>TextDecorationLine::Blink, |
| "spelling-error" if value.is_empty() => TextDecorationLine::SpellingError, |
| "grammar-error" if value.is_empty() => TextDecorationLine::GrammarError, |
| _ => return Err(location.new_unexpected_token_error( |
| cssparser::Token::Ident(ident.clone()) |
| )) |
| }) |
| }); |
| |
| if let Ok(flag) = flag { |
| value |= flag; |
| any = true; |
| } else { |
| break; |
| } |
| } |
| |
| if !any { |
| return Err(input.new_custom_error(ParserError::InvalidDeclaration)); |
| } |
| |
| Ok(value) |
| } |
| } |
| |
| impl ToCss for TextDecorationLine { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| if self.is_empty() { |
| return dest.write_str("none"); |
| } |
| |
| if self.contains(TextDecorationLine::SpellingError) { |
| return dest.write_str("spelling-error"); |
| } |
| |
| if self.contains(TextDecorationLine::GrammarError) { |
| return dest.write_str("grammar-error"); |
| } |
| |
| let mut needs_space = false; |
| macro_rules! val { |
| ($val: ident, $str: expr) => { |
| #[allow(unused_assignments)] |
| if self.contains(TextDecorationLine::$val) { |
| if needs_space { |
| dest.write_char(' ')?; |
| } |
| dest.write_str($str)?; |
| needs_space = true; |
| } |
| }; |
| } |
| |
| val!(Underline, "underline"); |
| val!(Overline, "overline"); |
| val!(LineThrough, "line-through"); |
| val!(Blink, "blink"); |
| Ok(()) |
| } |
| } |
| |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(untagged))] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| enum SerializedTextDecorationLine { |
| Exclusive(ExclusiveTextDecorationLine), |
| Other(Vec<OtherTextDecorationLine>), |
| } |
| |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| enum ExclusiveTextDecorationLine { |
| None, |
| SpellingError, |
| GrammarError, |
| } |
| |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| enum OtherTextDecorationLine { |
| Underline, |
| Overline, |
| LineThrough, |
| Blink, |
| } |
| |
| impl From<TextDecorationLine> for SerializedTextDecorationLine { |
| fn from(l: TextDecorationLine) -> Self { |
| if l.is_empty() { |
| return Self::Exclusive(ExclusiveTextDecorationLine::None); |
| } |
| |
| macro_rules! exclusive { |
| ($t: ident) => { |
| if l.contains(TextDecorationLine::$t) { |
| return Self::Exclusive(ExclusiveTextDecorationLine::$t); |
| } |
| }; |
| } |
| |
| exclusive!(SpellingError); |
| exclusive!(GrammarError); |
| |
| let mut v = Vec::new(); |
| macro_rules! other { |
| ($t: ident) => { |
| if l.contains(TextDecorationLine::$t) { |
| v.push(OtherTextDecorationLine::$t) |
| } |
| }; |
| } |
| |
| other!(Underline); |
| other!(Overline); |
| other!(LineThrough); |
| other!(Blink); |
| Self::Other(v) |
| } |
| } |
| |
| impl From<SerializedTextDecorationLine> for TextDecorationLine { |
| fn from(l: SerializedTextDecorationLine) -> Self { |
| match l { |
| SerializedTextDecorationLine::Exclusive(v) => match v { |
| ExclusiveTextDecorationLine::None => TextDecorationLine::empty(), |
| ExclusiveTextDecorationLine::SpellingError => TextDecorationLine::SpellingError, |
| ExclusiveTextDecorationLine::GrammarError => TextDecorationLine::GrammarError, |
| }, |
| SerializedTextDecorationLine::Other(v) => { |
| let mut res = TextDecorationLine::empty(); |
| for val in v { |
| res |= match val { |
| OtherTextDecorationLine::Underline => TextDecorationLine::Underline, |
| OtherTextDecorationLine::Overline => TextDecorationLine::Overline, |
| OtherTextDecorationLine::LineThrough => TextDecorationLine::LineThrough, |
| OtherTextDecorationLine::Blink => TextDecorationLine::Blink, |
| } |
| } |
| res |
| } |
| } |
| } |
| } |
| |
| #[cfg(feature = "jsonschema")] |
| #[cfg_attr(docsrs, doc(cfg(feature = "jsonschema")))] |
| impl<'a> schemars::JsonSchema for TextDecorationLine { |
| fn is_referenceable() -> bool { |
| true |
| } |
| |
| fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { |
| SerializedTextDecorationLine::json_schema(gen) |
| } |
| |
| fn schema_name() -> String { |
| "TextDecorationLine".into() |
| } |
| } |
| |
| enum_property! { |
| /// A value for the [text-decoration-style](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-style-property) property. |
| pub enum TextDecorationStyle { |
| /// A single line segment. |
| Solid, |
| /// Two parallel solid lines with some space between them. |
| Double, |
| /// A series of round dots. |
| Dotted, |
| /// A series of square-ended dashes. |
| Dashed, |
| /// A wavy line. |
| Wavy, |
| } |
| } |
| |
| impl Default for TextDecorationStyle { |
| fn default() -> TextDecorationStyle { |
| TextDecorationStyle::Solid |
| } |
| } |
| |
| /// A value for the [text-decoration-thickness](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-width-property) property. |
| #[derive(Debug, Clone, PartialEq, Parse, ToCss)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(tag = "type", content = "value", rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub enum TextDecorationThickness { |
| /// The UA chooses an appropriate thickness for text decoration lines. |
| Auto, |
| /// Use the thickness defined in the current font. |
| FromFont, |
| /// An explicit length. |
| LengthPercentage(LengthPercentage), |
| } |
| |
| impl Default for TextDecorationThickness { |
| fn default() -> TextDecorationThickness { |
| TextDecorationThickness::Auto |
| } |
| } |
| |
| define_shorthand! { |
| /// A value for the [text-decoration](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-property) shorthand property. |
| pub struct TextDecoration(VendorPrefix) { |
| /// The lines to display. |
| line: TextDecorationLine(TextDecorationLine, VendorPrefix), |
| /// The thickness of the lines. |
| thickness: TextDecorationThickness(TextDecorationThickness), |
| /// The style of the lines. |
| style: TextDecorationStyle(TextDecorationStyle, VendorPrefix), |
| /// The color of the lines. |
| color: TextDecorationColor(CssColor, VendorPrefix), |
| } |
| } |
| |
| impl<'i> Parse<'i> for TextDecoration { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| let mut line = None; |
| let mut thickness = None; |
| let mut style = None; |
| let mut color = None; |
| |
| loop { |
| macro_rules! prop { |
| ($key: ident, $type: ident) => { |
| if $key.is_none() { |
| if let Ok(val) = input.try_parse($type::parse) { |
| $key = Some(val); |
| continue; |
| } |
| } |
| }; |
| } |
| |
| prop!(line, TextDecorationLine); |
| prop!(thickness, TextDecorationThickness); |
| prop!(style, TextDecorationStyle); |
| prop!(color, CssColor); |
| break; |
| } |
| |
| Ok(TextDecoration { |
| line: line.unwrap_or_default(), |
| thickness: thickness.unwrap_or_default(), |
| style: style.unwrap_or_default(), |
| color: color.unwrap_or(CssColor::current_color()), |
| }) |
| } |
| } |
| |
| impl ToCss for TextDecoration { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| self.line.to_css(dest)?; |
| if self.line.is_empty() { |
| return Ok(()); |
| } |
| |
| let mut needs_space = true; |
| if self.thickness != TextDecorationThickness::default() { |
| dest.write_char(' ')?; |
| self.thickness.to_css(dest)?; |
| needs_space = true; |
| } |
| |
| if self.style != TextDecorationStyle::default() { |
| if needs_space { |
| dest.write_char(' ')?; |
| } |
| self.style.to_css(dest)?; |
| needs_space = true; |
| } |
| |
| if self.color != CssColor::current_color() { |
| if needs_space { |
| dest.write_char(' ')?; |
| } |
| self.color.to_css(dest)?; |
| } |
| |
| Ok(()) |
| } |
| } |
| |
| impl FallbackValues for TextDecoration { |
| fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> { |
| self |
| .color |
| .get_fallbacks(targets) |
| .into_iter() |
| .map(|color| TextDecoration { color, ..self.clone() }) |
| .collect() |
| } |
| } |
| |
| enum_property! { |
| /// A value for the [text-decoration-skip-ink](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-skip-ink-property) property. |
| pub enum TextDecorationSkipInk { |
| /// UAs may interrupt underlines and overlines. |
| Auto, |
| /// UAs must interrupt underlines and overlines. |
| None, |
| /// UA must draw continuous underlines and overlines. |
| All, |
| } |
| } |
| |
| enum_property! { |
| /// A keyword for the [text-emphasis-style](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-style-property) property. |
| /// |
| /// See [TextEmphasisStyle](TextEmphasisStyle). |
| pub enum TextEmphasisFillMode { |
| /// The shape is filled with solid color. |
| Filled, |
| /// The shape is hollow. |
| Open, |
| } |
| } |
| |
| enum_property! { |
| /// A text emphasis shape for the [text-emphasis-style](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-style-property) property. |
| /// |
| /// See [TextEmphasisStyle](TextEmphasisStyle). |
| pub enum TextEmphasisShape { |
| /// Display small circles as marks. |
| Dot, |
| /// Display large circles as marks. |
| Circle, |
| /// Display double circles as marks. |
| DoubleCircle, |
| /// Display triangles as marks. |
| Triangle, |
| /// Display sesames as marks. |
| Sesame, |
| } |
| } |
| |
| /// A value for the [text-emphasis-style](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-style-property) property. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(tag = "type", rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| pub enum TextEmphasisStyle<'i> { |
| /// No emphasis. |
| None, |
| /// Defines the fill and shape of the marks. |
| Keyword { |
| /// The fill mode for the marks. |
| fill: TextEmphasisFillMode, |
| /// The shape of the marks. |
| shape: Option<TextEmphasisShape>, |
| }, |
| /// Display the given string as marks. |
| #[cfg_attr( |
| feature = "serde", |
| serde(borrow, with = "crate::serialization::ValueWrapper::<CSSString>") |
| )] |
| String(CSSString<'i>), |
| } |
| |
| impl<'i> Default for TextEmphasisStyle<'i> { |
| fn default() -> TextEmphasisStyle<'i> { |
| TextEmphasisStyle::None |
| } |
| } |
| |
| impl<'i> Parse<'i> for TextEmphasisStyle<'i> { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() { |
| return Ok(TextEmphasisStyle::None); |
| } |
| |
| if let Ok(s) = input.try_parse(CSSString::parse) { |
| return Ok(TextEmphasisStyle::String(s)); |
| } |
| |
| let mut shape = input.try_parse(TextEmphasisShape::parse).ok(); |
| let fill = input.try_parse(TextEmphasisFillMode::parse).ok(); |
| if shape.is_none() { |
| shape = input.try_parse(TextEmphasisShape::parse).ok(); |
| } |
| |
| if shape.is_none() && fill.is_none() { |
| return Err(input.new_custom_error(ParserError::InvalidDeclaration)); |
| } |
| |
| let fill = fill.unwrap_or(TextEmphasisFillMode::Filled); |
| Ok(TextEmphasisStyle::Keyword { fill, shape }) |
| } |
| } |
| |
| impl<'i> ToCss for TextEmphasisStyle<'i> { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| match self { |
| TextEmphasisStyle::None => dest.write_str("none"), |
| TextEmphasisStyle::String(s) => s.to_css(dest), |
| TextEmphasisStyle::Keyword { fill, shape } => { |
| let mut needs_space = false; |
| if *fill != TextEmphasisFillMode::Filled || shape.is_none() { |
| fill.to_css(dest)?; |
| needs_space = true; |
| } |
| |
| if let Some(shape) = shape { |
| if needs_space { |
| dest.write_char(' ')?; |
| } |
| shape.to_css(dest)?; |
| } |
| Ok(()) |
| } |
| } |
| } |
| } |
| |
| define_shorthand! { |
| /// A value for the [text-emphasis](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-property) shorthand property. |
| pub struct TextEmphasis<'i>(VendorPrefix) { |
| /// The text emphasis style. |
| #[cfg_attr(feature = "serde", serde(borrow))] |
| style: TextEmphasisStyle(TextEmphasisStyle<'i>, VendorPrefix), |
| /// The text emphasis color. |
| color: TextEmphasisColor(CssColor, VendorPrefix), |
| } |
| } |
| |
| impl<'i> Parse<'i> for TextEmphasis<'i> { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| let mut style = None; |
| let mut color = None; |
| |
| loop { |
| if style.is_none() { |
| if let Ok(s) = input.try_parse(TextEmphasisStyle::parse) { |
| style = Some(s); |
| continue; |
| } |
| } |
| |
| if color.is_none() { |
| if let Ok(c) = input.try_parse(CssColor::parse) { |
| color = Some(c); |
| continue; |
| } |
| } |
| |
| break; |
| } |
| |
| Ok(TextEmphasis { |
| style: style.unwrap_or_default(), |
| color: color.unwrap_or(CssColor::current_color()), |
| }) |
| } |
| } |
| |
| impl<'i> ToCss for TextEmphasis<'i> { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| self.style.to_css(dest)?; |
| |
| if self.style != TextEmphasisStyle::None && self.color != CssColor::current_color() { |
| dest.write_char(' ')?; |
| self.color.to_css(dest)?; |
| } |
| |
| Ok(()) |
| } |
| } |
| |
| impl<'i> FallbackValues for TextEmphasis<'i> { |
| fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> { |
| self |
| .color |
| .get_fallbacks(targets) |
| .into_iter() |
| .map(|color| TextEmphasis { color, ..self.clone() }) |
| .collect() |
| } |
| } |
| |
| enum_property! { |
| /// A vertical position keyword for the [text-emphasis-position](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-position-property) property. |
| /// |
| /// See [TextEmphasisPosition](TextEmphasisPosition). |
| pub enum TextEmphasisPositionVertical { |
| /// Draw marks over the text in horizontal typographic modes. |
| Over, |
| /// Draw marks under the text in horizontal typographic modes. |
| Under, |
| } |
| } |
| |
| enum_property! { |
| /// A horizontal position keyword for the [text-emphasis-position](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-position-property) property. |
| /// |
| /// See [TextEmphasisPosition](TextEmphasisPosition). |
| pub enum TextEmphasisPositionHorizontal { |
| /// Draw marks to the right of the text in vertical typographic modes. |
| Left, |
| /// Draw marks to the left of the text in vertical typographic modes. |
| Right, |
| } |
| } |
| |
| /// A value for the [text-emphasis-position](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-position-property) property. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub struct TextEmphasisPosition { |
| /// The vertical position. |
| pub vertical: TextEmphasisPositionVertical, |
| /// The horizontal position. |
| pub horizontal: TextEmphasisPositionHorizontal, |
| } |
| |
| impl<'i> Parse<'i> for TextEmphasisPosition { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| if let Ok(horizontal) = input.try_parse(TextEmphasisPositionHorizontal::parse) { |
| let vertical = TextEmphasisPositionVertical::parse(input)?; |
| Ok(TextEmphasisPosition { horizontal, vertical }) |
| } else { |
| let vertical = TextEmphasisPositionVertical::parse(input)?; |
| let horizontal = input |
| .try_parse(TextEmphasisPositionHorizontal::parse) |
| .unwrap_or(TextEmphasisPositionHorizontal::Right); |
| Ok(TextEmphasisPosition { horizontal, vertical }) |
| } |
| } |
| } |
| |
| enum_property! { |
| /// A value for the [box-decoration-break](https://www.w3.org/TR/css-break-3/#break-decoration) property. |
| pub enum BoxDecorationBreak { |
| /// The element is rendered with no breaks present, and then sliced by the breaks afterward. |
| Slice, |
| /// Each box fragment is independently wrapped with the border, padding, and margin. |
| Clone, |
| } |
| } |
| |
| impl Default for BoxDecorationBreak { |
| fn default() -> Self { |
| BoxDecorationBreak::Slice |
| } |
| } |
| |
| impl ToCss for TextEmphasisPosition { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| self.vertical.to_css(dest)?; |
| if self.horizontal != TextEmphasisPositionHorizontal::Right { |
| dest.write_char(' ')?; |
| self.horizontal.to_css(dest)?; |
| } |
| Ok(()) |
| } |
| } |
| |
| #[derive(Default)] |
| pub(crate) struct TextDecorationHandler<'i> { |
| line: Option<(TextDecorationLine, VendorPrefix)>, |
| thickness: Option<TextDecorationThickness>, |
| style: Option<(TextDecorationStyle, VendorPrefix)>, |
| color: Option<(CssColor, VendorPrefix)>, |
| emphasis_style: Option<(TextEmphasisStyle<'i>, VendorPrefix)>, |
| emphasis_color: Option<(CssColor, VendorPrefix)>, |
| emphasis_position: Option<(TextEmphasisPosition, VendorPrefix)>, |
| has_any: bool, |
| } |
| |
| impl<'i> PropertyHandler<'i> for TextDecorationHandler<'i> { |
| fn handle_property( |
| &mut self, |
| property: &Property<'i>, |
| dest: &mut DeclarationList<'i>, |
| context: &mut PropertyHandlerContext<'i, '_>, |
| ) -> bool { |
| use Property::*; |
| |
| macro_rules! maybe_flush { |
| ($prop: ident, $val: expr, $vp: expr) => {{ |
| // If two vendor prefixes for the same property have different |
| // values, we need to flush what we have immediately to preserve order. |
| if let Some((val, prefixes)) = &self.$prop { |
| if val != $val && !prefixes.contains(*$vp) { |
| self.finalize(dest, context); |
| } |
| } |
| }}; |
| } |
| |
| macro_rules! property { |
| ($prop: ident, $val: expr, $vp: expr) => {{ |
| maybe_flush!($prop, $val, $vp); |
| |
| // Otherwise, update the value and add the prefix. |
| if let Some((val, prefixes)) = &mut self.$prop { |
| *val = $val.clone(); |
| *prefixes |= *$vp; |
| } else { |
| self.$prop = Some(($val.clone(), *$vp)); |
| self.has_any = true; |
| } |
| }}; |
| } |
| |
| match property { |
| TextDecorationLine(val, vp) => property!(line, val, vp), |
| TextDecorationThickness(val) => { |
| self.thickness = Some(val.clone()); |
| self.has_any = true; |
| } |
| TextDecorationStyle(val, vp) => property!(style, val, vp), |
| TextDecorationColor(val, vp) => property!(color, val, vp), |
| TextDecoration(val, vp) => { |
| maybe_flush!(line, &val.line, vp); |
| maybe_flush!(style, &val.style, vp); |
| maybe_flush!(color, &val.color, vp); |
| property!(line, &val.line, vp); |
| self.thickness = Some(val.thickness.clone()); |
| property!(style, &val.style, vp); |
| property!(color, &val.color, vp); |
| } |
| TextEmphasisStyle(val, vp) => property!(emphasis_style, val, vp), |
| TextEmphasisColor(val, vp) => property!(emphasis_color, val, vp), |
| TextEmphasis(val, vp) => { |
| maybe_flush!(emphasis_style, &val.style, vp); |
| maybe_flush!(emphasis_color, &val.color, vp); |
| property!(emphasis_style, &val.style, vp); |
| property!(emphasis_color, &val.color, vp); |
| } |
| TextEmphasisPosition(val, vp) => property!(emphasis_position, val, vp), |
| TextAlign(align) => { |
| use super::text::*; |
| macro_rules! logical { |
| ($ltr: ident, $rtl: ident) => {{ |
| let logical_supported = !context.should_compile_logical(compat::Feature::LogicalTextAlign); |
| if logical_supported { |
| dest.push(property.clone()); |
| } else { |
| context.add_logical_rule( |
| Property::TextAlign(TextAlign::$ltr), |
| Property::TextAlign(TextAlign::$rtl), |
| ); |
| } |
| }}; |
| } |
| |
| match align { |
| TextAlign::Start => logical!(Left, Right), |
| TextAlign::End => logical!(Right, Left), |
| _ => dest.push(property.clone()), |
| } |
| } |
| Unparsed(val) if is_text_decoration_property(&val.property_id) => { |
| self.finalize(dest, context); |
| let mut unparsed = val.get_prefixed(context.targets, Feature::TextDecoration); |
| context.add_unparsed_fallbacks(&mut unparsed); |
| dest.push(Property::Unparsed(unparsed)) |
| } |
| Unparsed(val) if is_text_emphasis_property(&val.property_id) => { |
| self.finalize(dest, context); |
| let mut unparsed = val.get_prefixed(context.targets, Feature::TextEmphasis); |
| context.add_unparsed_fallbacks(&mut unparsed); |
| dest.push(Property::Unparsed(unparsed)) |
| } |
| _ => return false, |
| } |
| |
| true |
| } |
| |
| fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) { |
| if !self.has_any { |
| return; |
| } |
| |
| self.has_any = false; |
| |
| let mut line = std::mem::take(&mut self.line); |
| let mut thickness = std::mem::take(&mut self.thickness); |
| let mut style = std::mem::take(&mut self.style); |
| let mut color = std::mem::take(&mut self.color); |
| let mut emphasis_style = std::mem::take(&mut self.emphasis_style); |
| let mut emphasis_color = std::mem::take(&mut self.emphasis_color); |
| let emphasis_position = std::mem::take(&mut self.emphasis_position); |
| |
| if let (Some((line, line_vp)), Some(thickness_val), Some((style, style_vp)), Some((color, color_vp))) = |
| (&mut line, &mut thickness, &mut style, &mut color) |
| { |
| let intersection = *line_vp | *style_vp | *color_vp; |
| if !intersection.is_empty() { |
| let mut prefix = intersection; |
| |
| // Some browsers don't support thickness in the shorthand property yet. |
| let supports_thickness = context.targets.is_compatible(compat::Feature::TextDecorationThicknessShorthand); |
| let mut decoration = TextDecoration { |
| line: line.clone(), |
| thickness: if supports_thickness { |
| thickness_val.clone() |
| } else { |
| TextDecorationThickness::default() |
| }, |
| style: style.clone(), |
| color: color.clone(), |
| }; |
| |
| // Only add prefixes if one of the new sub-properties was used |
| if prefix.contains(VendorPrefix::None) |
| && (*style != TextDecorationStyle::default() || *color != CssColor::current_color()) |
| { |
| prefix = context.targets.prefixes(VendorPrefix::None, Feature::TextDecoration); |
| |
| let fallbacks = decoration.get_fallbacks(context.targets); |
| for fallback in fallbacks { |
| dest.push(Property::TextDecoration(fallback, prefix)) |
| } |
| } |
| |
| dest.push(Property::TextDecoration(decoration, prefix)); |
| line_vp.remove(intersection); |
| style_vp.remove(intersection); |
| color_vp.remove(intersection); |
| if supports_thickness || *thickness_val == TextDecorationThickness::default() { |
| thickness = None; |
| } |
| } |
| } |
| |
| macro_rules! color { |
| ($key: ident, $prop: ident) => { |
| if let Some((mut val, vp)) = $key { |
| if !vp.is_empty() { |
| let prefix = context.targets.prefixes(vp, Feature::$prop); |
| if prefix.contains(VendorPrefix::None) { |
| let fallbacks = val.get_fallbacks(context.targets); |
| for fallback in fallbacks { |
| dest.push(Property::$prop(fallback, prefix)) |
| } |
| } |
| dest.push(Property::$prop(val, prefix)) |
| } |
| } |
| }; |
| } |
| |
| macro_rules! single_property { |
| ($key: ident, $prop: ident) => { |
| if let Some((val, vp)) = $key { |
| if !vp.is_empty() { |
| let prefix = context.targets.prefixes(vp, Feature::$prop); |
| dest.push(Property::$prop(val, prefix)) |
| } |
| } |
| }; |
| } |
| |
| single_property!(line, TextDecorationLine); |
| single_property!(style, TextDecorationStyle); |
| color!(color, TextDecorationColor); |
| |
| if let Some(thickness) = thickness { |
| // Percentages in the text-decoration-thickness property are based on 1em. |
| // If unsupported, compile this to a calc() instead. |
| match thickness { |
| TextDecorationThickness::LengthPercentage(LengthPercentage::Percentage(p)) |
| if should_compile!(context.targets, TextDecorationThicknessPercent) => |
| { |
| let calc = Calc::Function(Box::new(MathFunction::Calc(Calc::Product( |
| p.0, |
| Box::new(Calc::Value(Box::new(LengthPercentage::Dimension(LengthValue::Em(1.0))))), |
| )))); |
| let thickness = TextDecorationThickness::LengthPercentage(LengthPercentage::Calc(Box::new(calc))); |
| dest.push(Property::TextDecorationThickness(thickness)); |
| } |
| thickness => dest.push(Property::TextDecorationThickness(thickness)), |
| } |
| } |
| |
| if let (Some((style, style_vp)), Some((color, color_vp))) = (&mut emphasis_style, &mut emphasis_color) { |
| let intersection = *style_vp | *color_vp; |
| if !intersection.is_empty() { |
| let prefix = context.targets.prefixes(intersection, Feature::TextEmphasis); |
| let mut emphasis = TextEmphasis { |
| style: style.clone(), |
| color: color.clone(), |
| }; |
| |
| if prefix.contains(VendorPrefix::None) { |
| let fallbacks = emphasis.get_fallbacks(context.targets); |
| for fallback in fallbacks { |
| dest.push(Property::TextEmphasis(fallback, prefix)) |
| } |
| } |
| |
| dest.push(Property::TextEmphasis(emphasis, prefix)); |
| style_vp.remove(intersection); |
| color_vp.remove(intersection); |
| } |
| } |
| |
| single_property!(emphasis_style, TextEmphasisStyle); |
| color!(emphasis_color, TextEmphasisColor); |
| |
| if let Some((pos, vp)) = emphasis_position { |
| if !vp.is_empty() { |
| let mut prefix = context.targets.prefixes(vp, Feature::TextEmphasisPosition); |
| // Prefixed version does not support horizontal keyword. |
| if pos.horizontal != TextEmphasisPositionHorizontal::Right { |
| prefix = VendorPrefix::None; |
| } |
| dest.push(Property::TextEmphasisPosition(pos, prefix)) |
| } |
| } |
| } |
| } |
| |
| /// A value for the [text-shadow](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-shadow-property) property. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(rename_all = "camelCase") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub struct TextShadow { |
| /// The color of the text shadow. |
| pub color: CssColor, |
| /// The x offset of the text shadow. |
| pub x_offset: Length, |
| /// The y offset of the text shadow. |
| pub y_offset: Length, |
| /// The blur radius of the text shadow. |
| pub blur: Length, |
| /// The spread distance of the text shadow. |
| pub spread: Length, // added in Level 4 spec |
| } |
| |
| impl<'i> Parse<'i> for TextShadow { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| let mut color = None; |
| let mut lengths = None; |
| |
| loop { |
| if lengths.is_none() { |
| let value = input.try_parse::<_, _, ParseError<ParserError<'i>>>(|input| { |
| let horizontal = Length::parse(input)?; |
| let vertical = Length::parse(input)?; |
| let blur = input.try_parse(Length::parse).unwrap_or(Length::zero()); |
| let spread = input.try_parse(Length::parse).unwrap_or(Length::zero()); |
| Ok((horizontal, vertical, blur, spread)) |
| }); |
| |
| if let Ok(value) = value { |
| lengths = Some(value); |
| continue; |
| } |
| } |
| |
| if color.is_none() { |
| if let Ok(value) = input.try_parse(CssColor::parse) { |
| color = Some(value); |
| continue; |
| } |
| } |
| |
| break; |
| } |
| |
| let lengths = lengths.ok_or(input.new_error(BasicParseErrorKind::QualifiedRuleInvalid))?; |
| Ok(TextShadow { |
| color: color.unwrap_or(CssColor::current_color()), |
| x_offset: lengths.0, |
| y_offset: lengths.1, |
| blur: lengths.2, |
| spread: lengths.3, |
| }) |
| } |
| } |
| |
| impl ToCss for TextShadow { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| self.x_offset.to_css(dest)?; |
| dest.write_char(' ')?; |
| self.y_offset.to_css(dest)?; |
| |
| if self.blur != Length::zero() || self.spread != Length::zero() { |
| dest.write_char(' ')?; |
| self.blur.to_css(dest)?; |
| |
| if self.spread != Length::zero() { |
| dest.write_char(' ')?; |
| self.spread.to_css(dest)?; |
| } |
| } |
| |
| if self.color != CssColor::current_color() { |
| dest.write_char(' ')?; |
| self.color.to_css(dest)?; |
| } |
| |
| Ok(()) |
| } |
| } |
| |
| impl IsCompatible for TextShadow { |
| fn is_compatible(&self, browsers: Browsers) -> bool { |
| self.color.is_compatible(browsers) |
| && self.x_offset.is_compatible(browsers) |
| && self.y_offset.is_compatible(browsers) |
| && self.blur.is_compatible(browsers) |
| && self.spread.is_compatible(browsers) |
| } |
| } |
| |
| #[inline] |
| fn is_text_decoration_property(property_id: &PropertyId) -> bool { |
| match property_id { |
| PropertyId::TextDecorationLine(_) |
| | PropertyId::TextDecorationThickness |
| | PropertyId::TextDecorationStyle(_) |
| | PropertyId::TextDecorationColor(_) |
| | PropertyId::TextDecoration(_) => true, |
| _ => false, |
| } |
| } |
| |
| #[inline] |
| fn is_text_emphasis_property(property_id: &PropertyId) -> bool { |
| match property_id { |
| PropertyId::TextEmphasisStyle(_) |
| | PropertyId::TextEmphasisColor(_) |
| | PropertyId::TextEmphasis(_) |
| | PropertyId::TextEmphasisPosition(_) => true, |
| _ => false, |
| } |
| } |
| |
| impl FallbackValues for SmallVec<[TextShadow; 1]> { |
| fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> { |
| let mut fallbacks = ColorFallbackKind::empty(); |
| for shadow in self.iter() { |
| fallbacks |= shadow.color.get_necessary_fallbacks(targets); |
| } |
| |
| let mut res = Vec::new(); |
| if fallbacks.contains(ColorFallbackKind::RGB) { |
| let rgb = self |
| .iter() |
| .map(|shadow| TextShadow { |
| color: shadow.color.to_rgb().unwrap(), |
| ..shadow.clone() |
| }) |
| .collect(); |
| res.push(rgb); |
| } |
| |
| if fallbacks.contains(ColorFallbackKind::P3) { |
| let p3 = self |
| .iter() |
| .map(|shadow| TextShadow { |
| color: shadow.color.to_p3().unwrap(), |
| ..shadow.clone() |
| }) |
| .collect(); |
| res.push(p3); |
| } |
| |
| if fallbacks.contains(ColorFallbackKind::LAB) { |
| for shadow in self.iter_mut() { |
| shadow.color = shadow.color.to_lab().unwrap(); |
| } |
| } |
| |
| res |
| } |
| } |
| |
| enum_property! { |
| /// A value for the [direction](https://drafts.csswg.org/css-writing-modes-3/#direction) property. |
| pub enum Direction { |
| /// This value sets inline base direction (bidi directionality) to line-left-to-line-right. |
| Ltr, |
| /// This value sets inline base direction (bidi directionality) to line-right-to-line-left. |
| Rtl, |
| } |
| } |
| |
| enum_property! { |
| /// A value for the [unicode-bidi](https://drafts.csswg.org/css-writing-modes-3/#unicode-bidi) property. |
| pub enum UnicodeBidi { |
| /// The box does not open an additional level of embedding. |
| Normal, |
| /// If the box is inline, this value creates a directional embedding by opening an additional level of embedding. |
| Embed, |
| /// On an inline box, this bidi-isolates its contents. |
| Isolate, |
| /// This value puts the box’s immediate inline content in a directional override. |
| BidiOverride, |
| /// This combines the isolation behavior of isolate with the directional override behavior of bidi-override. |
| IsolateOverride, |
| /// This value behaves as isolate except that the base directionality is determined using a heuristic rather than the direction property. |
| Plaintext, |
| } |
| } |