//! 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>> { 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(&self, dest: &mut Printer) -> 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 for SerializedTextTransformOther { fn from(t: TextTransformOther) -> Self { Self { full_width: t.contains(TextTransformOther::FullWidth), full_size_kana: t.contains(TextTransformOther::FullSizeKana), } } } impl From 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>> { 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(&self, dest: &mut Printer) -> 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>> { 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(&self, dest: &mut Printer) -> 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>> { 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(&self, dest: &mut Printer) -> 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), } #[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 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 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>> { 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(&self, dest: &mut Printer) -> 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 .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, }, /// Display the given string as marks. #[cfg_attr( feature = "serde", serde(borrow, with = "crate::serialization::ValueWrapper::") )] 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>> { 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(&self, dest: &mut Printer) -> 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>> { 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(&self, dest: &mut Printer) -> 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 .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>> { 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(&self, dest: &mut Printer) -> 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, 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>> { let mut color = None; let mut lengths = None; loop { if lengths.is_none() { let value = input.try_parse::<_, _, ParseError>>(|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(&self, dest: &mut Printer) -> 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 { 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, } }