blob: 622b870fa9f43db51fdddf408c2f8bb93cde7b1f [file] [edit]
//! 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,
}
}