lightningcss/properties/
text.rs

1//! CSS properties related to text.
2
3#![allow(non_upper_case_globals)]
4
5use super::{Property, PropertyId};
6use crate::compat;
7use crate::context::PropertyHandlerContext;
8use crate::declaration::{DeclarationBlock, DeclarationList};
9use crate::error::{ParserError, PrinterError};
10use crate::macros::{define_shorthand, enum_property};
11use crate::prefixes::Feature;
12use crate::printer::Printer;
13use crate::targets::{should_compile, Browsers, Targets};
14use crate::traits::{FallbackValues, IsCompatible, Parse, PropertyHandler, Shorthand, ToCss, Zero};
15use crate::values::calc::{Calc, MathFunction};
16use crate::values::color::{ColorFallbackKind, CssColor};
17use crate::values::length::{Length, LengthPercentage, LengthValue};
18use crate::values::percentage::Percentage;
19use crate::values::string::CSSString;
20use crate::vendor_prefix::VendorPrefix;
21#[cfg(feature = "visitor")]
22use crate::visitor::Visit;
23use bitflags::bitflags;
24use cssparser::*;
25use smallvec::SmallVec;
26
27enum_property! {
28  /// Defines how text case should be transformed in the
29  /// [text-transform](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-transform-property) property.
30  pub enum TextTransformCase {
31    /// Text should not be transformed.
32    None,
33    /// Text should be uppercased.
34    Uppercase,
35    /// Text should be lowercased.
36    Lowercase,
37    /// Each word should be capitalized.
38    Capitalize,
39  }
40}
41
42impl Default for TextTransformCase {
43  fn default() -> TextTransformCase {
44    TextTransformCase::None
45  }
46}
47
48bitflags! {
49  /// Defines how ideographic characters should be transformed in the
50  /// [text-transform](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-transform-property) property.
51  ///
52  /// All combinations of flags is supported.
53  #[cfg_attr(feature = "visitor", derive(Visit))]
54  #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(from = "SerializedTextTransformOther", into = "SerializedTextTransformOther"))]
55  #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
56  pub struct TextTransformOther: u8 {
57    /// Puts all typographic character units in full-width form.
58    const FullWidth    = 0b00000001;
59    /// Converts all small Kana characters to the equivalent full-size Kana.
60    const FullSizeKana = 0b00000010;
61  }
62}
63
64impl<'i> Parse<'i> for TextTransformOther {
65  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
66    let location = input.current_source_location();
67    let ident = input.expect_ident()?;
68    match_ignore_ascii_case! { &ident,
69      "full-width" => Ok(TextTransformOther::FullWidth),
70      "full-size-kana" => Ok(TextTransformOther::FullSizeKana),
71      _ => Err(location.new_unexpected_token_error(
72        cssparser::Token::Ident(ident.clone())
73      ))
74    }
75  }
76}
77
78impl ToCss for TextTransformOther {
79  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
80  where
81    W: std::fmt::Write,
82  {
83    let mut needs_space = false;
84    if self.contains(TextTransformOther::FullWidth) {
85      dest.write_str("full-width")?;
86      needs_space = true;
87    }
88
89    if self.contains(TextTransformOther::FullSizeKana) {
90      if needs_space {
91        dest.write_char(' ')?;
92      }
93      dest.write_str("full-size-kana")?;
94    }
95
96    Ok(())
97  }
98}
99
100#[cfg_attr(
101  feature = "serde",
102  derive(serde::Serialize, serde::Deserialize),
103  serde(rename_all = "camelCase")
104)]
105#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
106#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
107struct SerializedTextTransformOther {
108  /// Puts all typographic character units in full-width form.
109  full_width: bool,
110  /// Converts all small Kana characters to the equivalent full-size Kana.
111  full_size_kana: bool,
112}
113
114impl From<TextTransformOther> for SerializedTextTransformOther {
115  fn from(t: TextTransformOther) -> Self {
116    Self {
117      full_width: t.contains(TextTransformOther::FullWidth),
118      full_size_kana: t.contains(TextTransformOther::FullSizeKana),
119    }
120  }
121}
122
123impl From<SerializedTextTransformOther> for TextTransformOther {
124  fn from(t: SerializedTextTransformOther) -> Self {
125    let mut res = TextTransformOther::empty();
126    if t.full_width {
127      res |= TextTransformOther::FullWidth;
128    }
129    if t.full_size_kana {
130      res |= TextTransformOther::FullSizeKana;
131    }
132    res
133  }
134}
135
136#[cfg(feature = "jsonschema")]
137#[cfg_attr(docsrs, doc(cfg(feature = "jsonschema")))]
138impl<'a> schemars::JsonSchema for TextTransformOther {
139  fn is_referenceable() -> bool {
140    true
141  }
142
143  fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
144    SerializedTextTransformOther::json_schema(gen)
145  }
146
147  fn schema_name() -> String {
148    "TextTransformOther".into()
149  }
150}
151
152/// A value for the [text-transform](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-transform-property) property.
153#[derive(Debug, Clone, PartialEq)]
154#[cfg_attr(feature = "visitor", derive(Visit))]
155#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
156#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
157#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
158pub struct TextTransform {
159  /// How case should be transformed.
160  pub case: TextTransformCase,
161  /// How ideographic characters should be transformed.
162  #[cfg_attr(feature = "serde", serde(flatten))]
163  pub other: TextTransformOther,
164}
165
166impl<'i> Parse<'i> for TextTransform {
167  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
168    let mut case = None;
169    let mut other = TextTransformOther::empty();
170
171    loop {
172      if case.is_none() {
173        if let Ok(c) = input.try_parse(TextTransformCase::parse) {
174          case = Some(c);
175          if c == TextTransformCase::None {
176            other = TextTransformOther::empty();
177            break;
178          }
179          continue;
180        }
181      }
182
183      if let Ok(o) = input.try_parse(TextTransformOther::parse) {
184        other |= o;
185        continue;
186      }
187
188      break;
189    }
190
191    Ok(TextTransform {
192      case: case.unwrap_or_default(),
193      other,
194    })
195  }
196}
197
198impl ToCss for TextTransform {
199  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
200  where
201    W: std::fmt::Write,
202  {
203    let mut needs_space = false;
204    if self.case != TextTransformCase::None || self.other.is_empty() {
205      self.case.to_css(dest)?;
206      needs_space = true;
207    }
208
209    if !self.other.is_empty() {
210      if needs_space {
211        dest.write_char(' ')?;
212      }
213      self.other.to_css(dest)?;
214    }
215    Ok(())
216  }
217}
218
219enum_property! {
220  /// A value for the [white-space](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#white-space-property) property.
221  pub enum WhiteSpace {
222    /// Sequences of white space are collapsed into a single character.
223    "normal": Normal,
224    /// White space is not collapsed.
225    "pre": Pre,
226    /// White space is collapsed, but no line wrapping occurs.
227    "nowrap": NoWrap,
228    /// White space is preserved, but line wrapping occurs.
229    "pre-wrap": PreWrap,
230    /// Like pre-wrap, but with different line breaking rules.
231    "break-spaces": BreakSpaces,
232    /// White space is collapsed, but with different line breaking rules.
233    "pre-line": PreLine,
234  }
235}
236
237enum_property! {
238  /// A value for the [word-break](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#word-break-property) property.
239  pub enum WordBreak {
240    /// Words break according to their customary rules.
241    Normal,
242    /// Breaking is forbidden within “words”.
243    KeepAll,
244    /// Breaking is allowed within “words”.
245    BreakAll,
246    /// Breaking is allowed if there is no otherwise acceptable break points in a line.
247    BreakWord,
248  }
249}
250
251enum_property! {
252  /// A value for the [line-break](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#line-break-property) property.
253  pub enum LineBreak {
254    /// The UA determines the set of line-breaking restrictions to use.
255    Auto,
256    /// Breaks text using the least restrictive set of line-breaking rules.
257    Loose,
258    /// Breaks text using the most common set of line-breaking rules.
259    Normal,
260    /// Breaks text using the most stringent set of line-breaking rules.
261    Strict,
262    /// There is a soft wrap opportunity around every typographic character unit.
263    Anywhere,
264  }
265}
266enum_property! {
267  /// A value for the [hyphens](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#hyphenation) property.
268  pub enum Hyphens {
269    /// Words are not hyphenated.
270    None,
271    /// Words are only hyphenated where there are characters inside the word that explicitly suggest hyphenation opportunities.
272    Manual,
273    /// Words may be broken at hyphenation opportunities determined automatically by the UA.
274    Auto,
275  }
276}
277
278enum_property! {
279  /// A value for the [overflow-wrap](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#overflow-wrap-property) property.
280  pub enum OverflowWrap {
281    /// Lines may break only at allowed break points.
282    Normal,
283    /// Breaking is allowed if there is no otherwise acceptable break points in a line.
284    Anywhere,
285    /// As for anywhere except that soft wrap opportunities introduced by break-word are
286    /// not considered when calculating min-content intrinsic sizes.
287    BreakWord,
288  }
289}
290
291enum_property! {
292  /// A value for the [text-align](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-align-property) property.
293  pub enum TextAlign {
294    /// Inline-level content is aligned to the start edge of the line box.
295    Start,
296    /// Inline-level content is aligned to the end edge of the line box.
297    End,
298    /// Inline-level content is aligned to the line-left edge of the line box.
299    Left,
300    /// Inline-level content is aligned to the line-right edge of the line box.
301    Right,
302    /// Inline-level content is centered within the line box.
303    Center,
304    /// Text is justified according to the method specified by the text-justify property.
305    Justify,
306    /// Matches the parent element.
307    MatchParent,
308    /// Same as justify, but also justifies the last line.
309    JustifyAll,
310  }
311}
312
313enum_property! {
314  /// A value for the [text-align-last](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-align-last-property) property.
315  pub enum TextAlignLast {
316    /// Content on the affected line is aligned per `text-align-all` unless set to `justify`, in which case it is start-aligned.
317    Auto,
318    /// Inline-level content is aligned to the start edge of the line box.
319    Start,
320    /// Inline-level content is aligned to the end edge of the line box.
321    End,
322    /// Inline-level content is aligned to the line-left edge of the line box.
323    Left,
324    /// Inline-level content is aligned to the line-right edge of the line box.
325    Right,
326    /// Inline-level content is centered within the line box.
327    Center,
328    /// Text is justified according to the method specified by the text-justify property.
329    Justify,
330    /// Matches the parent element.
331    MatchParent,
332  }
333}
334
335enum_property! {
336  /// A value for the [text-justify](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-justify-property) property.
337  pub enum TextJustify {
338    /// The UA determines the justification algorithm to follow.
339    Auto,
340    /// Justification is disabled.
341    None,
342    /// Justification adjusts spacing at word separators only.
343    InterWord,
344    /// Justification adjusts spacing between each character.
345    InterCharacter,
346  }
347}
348
349/// A value for the [word-spacing](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#word-spacing-property)
350/// and [letter-spacing](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#letter-spacing-property) properties.
351#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
352#[cfg_attr(feature = "visitor", derive(Visit))]
353#[cfg_attr(
354  feature = "serde",
355  derive(serde::Serialize, serde::Deserialize),
356  serde(tag = "type", content = "value", rename_all = "kebab-case")
357)]
358#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
359#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
360pub enum Spacing {
361  /// No additional spacing is applied.
362  Normal,
363  /// Additional spacing between each word or letter.
364  Length(Length),
365}
366
367/// A value for the [text-indent](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-indent-property) property.
368#[derive(Debug, Clone, PartialEq)]
369#[cfg_attr(feature = "visitor", derive(Visit))]
370#[cfg_attr(
371  feature = "serde",
372  derive(serde::Serialize, serde::Deserialize),
373  serde(rename_all = "camelCase")
374)]
375#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
376#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
377pub struct TextIndent {
378  /// The amount to indent.
379  pub value: LengthPercentage,
380  /// Inverts which lines are affected.
381  pub hanging: bool,
382  /// Affects the first line after each hard break.
383  pub each_line: bool,
384}
385
386impl<'i> Parse<'i> for TextIndent {
387  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
388    let mut value = None;
389    let mut hanging = false;
390    let mut each_line = false;
391
392    loop {
393      if value.is_none() {
394        if let Ok(val) = input.try_parse(LengthPercentage::parse) {
395          value = Some(val);
396          continue;
397        }
398      }
399
400      if !hanging {
401        if input.try_parse(|input| input.expect_ident_matching("hanging")).is_ok() {
402          hanging = true;
403          continue;
404        }
405      }
406
407      if !each_line {
408        if input.try_parse(|input| input.expect_ident_matching("each-line")).is_ok() {
409          each_line = true;
410          continue;
411        }
412      }
413
414      break;
415    }
416
417    if let Some(value) = value {
418      Ok(TextIndent {
419        value,
420        hanging,
421        each_line,
422      })
423    } else {
424      Err(input.new_custom_error(ParserError::InvalidDeclaration))
425    }
426  }
427}
428
429impl ToCss for TextIndent {
430  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
431  where
432    W: std::fmt::Write,
433  {
434    self.value.to_css(dest)?;
435    if self.hanging {
436      dest.write_str(" hanging")?;
437    }
438    if self.each_line {
439      dest.write_str(" each-line")?;
440    }
441    Ok(())
442  }
443}
444
445/// A value for the [text-size-adjust](https://w3c.github.io/csswg-drafts/css-size-adjust/#adjustment-control) property.
446#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
447#[cfg_attr(feature = "visitor", derive(Visit))]
448#[cfg_attr(
449  feature = "serde",
450  derive(serde::Serialize, serde::Deserialize),
451  serde(tag = "type", content = "value", rename_all = "kebab-case")
452)]
453#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
454#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
455pub enum TextSizeAdjust {
456  /// Use the default size adjustment when displaying on a small device.
457  Auto,
458  /// No size adjustment when displaying on a small device.
459  None,
460  /// When displaying on a small device, the font size is multiplied by this percentage.
461  Percentage(Percentage),
462}
463
464bitflags! {
465  /// A value for the [text-decoration-line](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-line-property) property.
466  ///
467  /// Multiple lines may be specified by combining the flags.
468  #[cfg_attr(feature = "visitor", derive(Visit))]
469  #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(from = "SerializedTextDecorationLine", into = "SerializedTextDecorationLine"))]
470  #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
471  #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
472  pub struct TextDecorationLine: u8 {
473    /// Each line of text is underlined.
474    const Underline     = 0b00000001;
475    /// Each line of text has a line over it.
476    const Overline      = 0b00000010;
477    /// Each line of text has a line through the middle.
478    const LineThrough   = 0b00000100;
479    /// The text blinks.
480    const Blink         = 0b00001000;
481    /// The text is decorated as a spelling error.
482    const SpellingError = 0b00010000;
483    /// The text is decorated as a grammar error.
484    const GrammarError  = 0b00100000;
485  }
486}
487
488impl Default for TextDecorationLine {
489  fn default() -> TextDecorationLine {
490    TextDecorationLine::empty()
491  }
492}
493
494impl<'i> Parse<'i> for TextDecorationLine {
495  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
496    let mut value = TextDecorationLine::empty();
497    let mut any = false;
498
499    loop {
500      let flag: Result<_, ParseError<'i, ParserError<'i>>> = input.try_parse(|input| {
501        let location = input.current_source_location();
502        let ident = input.expect_ident()?;
503        Ok(match_ignore_ascii_case! { &ident,
504          "none" if value.is_empty() => TextDecorationLine::empty(),
505          "underline" => TextDecorationLine::Underline,
506          "overline" => TextDecorationLine::Overline,
507          "line-through" => TextDecorationLine::LineThrough,
508          "blink" =>TextDecorationLine::Blink,
509          "spelling-error" if value.is_empty() => TextDecorationLine::SpellingError,
510          "grammar-error" if value.is_empty() => TextDecorationLine::GrammarError,
511          _ => return Err(location.new_unexpected_token_error(
512            cssparser::Token::Ident(ident.clone())
513          ))
514        })
515      });
516
517      if let Ok(flag) = flag {
518        value |= flag;
519        any = true;
520      } else {
521        break;
522      }
523    }
524
525    if !any {
526      return Err(input.new_custom_error(ParserError::InvalidDeclaration));
527    }
528
529    Ok(value)
530  }
531}
532
533impl ToCss for TextDecorationLine {
534  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
535  where
536    W: std::fmt::Write,
537  {
538    if self.is_empty() {
539      return dest.write_str("none");
540    }
541
542    if self.contains(TextDecorationLine::SpellingError) {
543      return dest.write_str("spelling-error");
544    }
545
546    if self.contains(TextDecorationLine::GrammarError) {
547      return dest.write_str("grammar-error");
548    }
549
550    let mut needs_space = false;
551    macro_rules! val {
552      ($val: ident, $str: expr) => {
553        #[allow(unused_assignments)]
554        if self.contains(TextDecorationLine::$val) {
555          if needs_space {
556            dest.write_char(' ')?;
557          }
558          dest.write_str($str)?;
559          needs_space = true;
560        }
561      };
562    }
563
564    val!(Underline, "underline");
565    val!(Overline, "overline");
566    val!(LineThrough, "line-through");
567    val!(Blink, "blink");
568    Ok(())
569  }
570}
571
572#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(untagged))]
573#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
574enum SerializedTextDecorationLine {
575  Exclusive(ExclusiveTextDecorationLine),
576  Other(Vec<OtherTextDecorationLine>),
577}
578
579#[cfg_attr(
580  feature = "serde",
581  derive(serde::Serialize, serde::Deserialize),
582  serde(rename_all = "kebab-case")
583)]
584#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
585enum ExclusiveTextDecorationLine {
586  None,
587  SpellingError,
588  GrammarError,
589}
590
591#[cfg_attr(
592  feature = "serde",
593  derive(serde::Serialize, serde::Deserialize),
594  serde(rename_all = "kebab-case")
595)]
596#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
597enum OtherTextDecorationLine {
598  Underline,
599  Overline,
600  LineThrough,
601  Blink,
602}
603
604impl From<TextDecorationLine> for SerializedTextDecorationLine {
605  fn from(l: TextDecorationLine) -> Self {
606    if l.is_empty() {
607      return Self::Exclusive(ExclusiveTextDecorationLine::None);
608    }
609
610    macro_rules! exclusive {
611      ($t: ident) => {
612        if l.contains(TextDecorationLine::$t) {
613          return Self::Exclusive(ExclusiveTextDecorationLine::$t);
614        }
615      };
616    }
617
618    exclusive!(SpellingError);
619    exclusive!(GrammarError);
620
621    let mut v = Vec::new();
622    macro_rules! other {
623      ($t: ident) => {
624        if l.contains(TextDecorationLine::$t) {
625          v.push(OtherTextDecorationLine::$t)
626        }
627      };
628    }
629
630    other!(Underline);
631    other!(Overline);
632    other!(LineThrough);
633    other!(Blink);
634    Self::Other(v)
635  }
636}
637
638impl From<SerializedTextDecorationLine> for TextDecorationLine {
639  fn from(l: SerializedTextDecorationLine) -> Self {
640    match l {
641      SerializedTextDecorationLine::Exclusive(v) => match v {
642        ExclusiveTextDecorationLine::None => TextDecorationLine::empty(),
643        ExclusiveTextDecorationLine::SpellingError => TextDecorationLine::SpellingError,
644        ExclusiveTextDecorationLine::GrammarError => TextDecorationLine::GrammarError,
645      },
646      SerializedTextDecorationLine::Other(v) => {
647        let mut res = TextDecorationLine::empty();
648        for val in v {
649          res |= match val {
650            OtherTextDecorationLine::Underline => TextDecorationLine::Underline,
651            OtherTextDecorationLine::Overline => TextDecorationLine::Overline,
652            OtherTextDecorationLine::LineThrough => TextDecorationLine::LineThrough,
653            OtherTextDecorationLine::Blink => TextDecorationLine::Blink,
654          }
655        }
656        res
657      }
658    }
659  }
660}
661
662#[cfg(feature = "jsonschema")]
663#[cfg_attr(docsrs, doc(cfg(feature = "jsonschema")))]
664impl<'a> schemars::JsonSchema for TextDecorationLine {
665  fn is_referenceable() -> bool {
666    true
667  }
668
669  fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
670    SerializedTextDecorationLine::json_schema(gen)
671  }
672
673  fn schema_name() -> String {
674    "TextDecorationLine".into()
675  }
676}
677
678enum_property! {
679  /// A value for the [text-decoration-style](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-style-property) property.
680  pub enum TextDecorationStyle {
681    /// A single line segment.
682    Solid,
683    /// Two parallel solid lines with some space between them.
684    Double,
685    /// A series of round dots.
686    Dotted,
687    /// A series of square-ended dashes.
688    Dashed,
689    /// A wavy line.
690    Wavy,
691  }
692}
693
694impl Default for TextDecorationStyle {
695  fn default() -> TextDecorationStyle {
696    TextDecorationStyle::Solid
697  }
698}
699
700/// A value for the [text-decoration-thickness](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-width-property) property.
701#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
702#[cfg_attr(feature = "visitor", derive(Visit))]
703#[cfg_attr(
704  feature = "serde",
705  derive(serde::Serialize, serde::Deserialize),
706  serde(tag = "type", content = "value", rename_all = "kebab-case")
707)]
708#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
709#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
710pub enum TextDecorationThickness {
711  /// The UA chooses an appropriate thickness for text decoration lines.
712  Auto,
713  /// Use the thickness defined in the current font.
714  FromFont,
715  /// An explicit length.
716  LengthPercentage(LengthPercentage),
717}
718
719impl Default for TextDecorationThickness {
720  fn default() -> TextDecorationThickness {
721    TextDecorationThickness::Auto
722  }
723}
724
725define_shorthand! {
726  /// A value for the [text-decoration](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-property) shorthand property.
727  pub struct TextDecoration(VendorPrefix) {
728    /// The lines to display.
729    line: TextDecorationLine(TextDecorationLine, VendorPrefix),
730    /// The thickness of the lines.
731    thickness: TextDecorationThickness(TextDecorationThickness),
732    /// The style of the lines.
733    style: TextDecorationStyle(TextDecorationStyle, VendorPrefix),
734    /// The color of the lines.
735    color: TextDecorationColor(CssColor, VendorPrefix),
736  }
737}
738
739impl<'i> Parse<'i> for TextDecoration {
740  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
741    let mut line = None;
742    let mut thickness = None;
743    let mut style = None;
744    let mut color = None;
745
746    loop {
747      macro_rules! prop {
748        ($key: ident, $type: ident) => {
749          if $key.is_none() {
750            if let Ok(val) = input.try_parse($type::parse) {
751              $key = Some(val);
752              continue;
753            }
754          }
755        };
756      }
757
758      prop!(line, TextDecorationLine);
759      prop!(thickness, TextDecorationThickness);
760      prop!(style, TextDecorationStyle);
761      prop!(color, CssColor);
762      break;
763    }
764
765    Ok(TextDecoration {
766      line: line.unwrap_or_default(),
767      thickness: thickness.unwrap_or_default(),
768      style: style.unwrap_or_default(),
769      color: color.unwrap_or(CssColor::current_color()),
770    })
771  }
772}
773
774impl ToCss for TextDecoration {
775  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
776  where
777    W: std::fmt::Write,
778  {
779    self.line.to_css(dest)?;
780    if self.line.is_empty() {
781      return Ok(());
782    }
783
784    let mut needs_space = true;
785    if self.thickness != TextDecorationThickness::default() {
786      dest.write_char(' ')?;
787      self.thickness.to_css(dest)?;
788      needs_space = true;
789    }
790
791    if self.style != TextDecorationStyle::default() {
792      if needs_space {
793        dest.write_char(' ')?;
794      }
795      self.style.to_css(dest)?;
796      needs_space = true;
797    }
798
799    if self.color != CssColor::current_color() {
800      if needs_space {
801        dest.write_char(' ')?;
802      }
803      self.color.to_css(dest)?;
804    }
805
806    Ok(())
807  }
808}
809
810impl FallbackValues for TextDecoration {
811  fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
812    self
813      .color
814      .get_fallbacks(targets)
815      .into_iter()
816      .map(|color| TextDecoration { color, ..self.clone() })
817      .collect()
818  }
819}
820
821enum_property! {
822  /// 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.
823  pub enum TextDecorationSkipInk {
824    /// UAs may interrupt underlines and overlines.
825    Auto,
826    /// UAs must interrupt underlines and overlines.
827    None,
828    /// UA must draw continuous underlines and overlines.
829    All,
830  }
831}
832
833enum_property! {
834  /// A keyword for the [text-emphasis-style](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-style-property) property.
835  ///
836  /// See [TextEmphasisStyle](TextEmphasisStyle).
837  pub enum TextEmphasisFillMode {
838    /// The shape is filled with solid color.
839    Filled,
840    /// The shape is hollow.
841    Open,
842  }
843}
844
845enum_property! {
846  /// 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.
847  ///
848  /// See [TextEmphasisStyle](TextEmphasisStyle).
849  pub enum TextEmphasisShape {
850    /// Display small circles as marks.
851    Dot,
852    /// Display large circles as marks.
853    Circle,
854    /// Display double circles as marks.
855    DoubleCircle,
856    /// Display triangles as marks.
857    Triangle,
858    /// Display sesames as marks.
859    Sesame,
860  }
861}
862
863/// A value for the [text-emphasis-style](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-style-property) property.
864#[derive(Debug, Clone, PartialEq)]
865#[cfg_attr(feature = "visitor", derive(Visit))]
866#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
867#[cfg_attr(
868  feature = "serde",
869  derive(serde::Serialize, serde::Deserialize),
870  serde(tag = "type", rename_all = "kebab-case")
871)]
872#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
873pub enum TextEmphasisStyle<'i> {
874  /// No emphasis.
875  None,
876  /// Defines the fill and shape of the marks.
877  Keyword {
878    /// The fill mode for the marks.
879    fill: TextEmphasisFillMode,
880    /// The shape of the marks.
881    shape: Option<TextEmphasisShape>,
882  },
883  /// Display the given string as marks.
884  #[cfg_attr(
885    feature = "serde",
886    serde(borrow, with = "crate::serialization::ValueWrapper::<CSSString>")
887  )]
888  String(CSSString<'i>),
889}
890
891impl<'i> Default for TextEmphasisStyle<'i> {
892  fn default() -> TextEmphasisStyle<'i> {
893    TextEmphasisStyle::None
894  }
895}
896
897impl<'i> Parse<'i> for TextEmphasisStyle<'i> {
898  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
899    if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() {
900      return Ok(TextEmphasisStyle::None);
901    }
902
903    if let Ok(s) = input.try_parse(CSSString::parse) {
904      return Ok(TextEmphasisStyle::String(s));
905    }
906
907    let mut shape = input.try_parse(TextEmphasisShape::parse).ok();
908    let fill = input.try_parse(TextEmphasisFillMode::parse).ok();
909    if shape.is_none() {
910      shape = input.try_parse(TextEmphasisShape::parse).ok();
911    }
912
913    if shape.is_none() && fill.is_none() {
914      return Err(input.new_custom_error(ParserError::InvalidDeclaration));
915    }
916
917    let fill = fill.unwrap_or(TextEmphasisFillMode::Filled);
918    Ok(TextEmphasisStyle::Keyword { fill, shape })
919  }
920}
921
922impl<'i> ToCss for TextEmphasisStyle<'i> {
923  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
924  where
925    W: std::fmt::Write,
926  {
927    match self {
928      TextEmphasisStyle::None => dest.write_str("none"),
929      TextEmphasisStyle::String(s) => s.to_css(dest),
930      TextEmphasisStyle::Keyword { fill, shape } => {
931        let mut needs_space = false;
932        if *fill != TextEmphasisFillMode::Filled || shape.is_none() {
933          fill.to_css(dest)?;
934          needs_space = true;
935        }
936
937        if let Some(shape) = shape {
938          if needs_space {
939            dest.write_char(' ')?;
940          }
941          shape.to_css(dest)?;
942        }
943        Ok(())
944      }
945    }
946  }
947}
948
949define_shorthand! {
950  /// A value for the [text-emphasis](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-property) shorthand property.
951  pub struct TextEmphasis<'i>(VendorPrefix) {
952    /// The text emphasis style.
953    #[cfg_attr(feature = "serde", serde(borrow))]
954    style: TextEmphasisStyle(TextEmphasisStyle<'i>, VendorPrefix),
955    /// The text emphasis color.
956    color: TextEmphasisColor(CssColor, VendorPrefix),
957  }
958}
959
960impl<'i> Parse<'i> for TextEmphasis<'i> {
961  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
962    let mut style = None;
963    let mut color = None;
964
965    loop {
966      if style.is_none() {
967        if let Ok(s) = input.try_parse(TextEmphasisStyle::parse) {
968          style = Some(s);
969          continue;
970        }
971      }
972
973      if color.is_none() {
974        if let Ok(c) = input.try_parse(CssColor::parse) {
975          color = Some(c);
976          continue;
977        }
978      }
979
980      break;
981    }
982
983    Ok(TextEmphasis {
984      style: style.unwrap_or_default(),
985      color: color.unwrap_or(CssColor::current_color()),
986    })
987  }
988}
989
990impl<'i> ToCss for TextEmphasis<'i> {
991  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
992  where
993    W: std::fmt::Write,
994  {
995    self.style.to_css(dest)?;
996
997    if self.style != TextEmphasisStyle::None && self.color != CssColor::current_color() {
998      dest.write_char(' ')?;
999      self.color.to_css(dest)?;
1000    }
1001
1002    Ok(())
1003  }
1004}
1005
1006impl<'i> FallbackValues for TextEmphasis<'i> {
1007  fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
1008    self
1009      .color
1010      .get_fallbacks(targets)
1011      .into_iter()
1012      .map(|color| TextEmphasis { color, ..self.clone() })
1013      .collect()
1014  }
1015}
1016
1017enum_property! {
1018  /// 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.
1019  ///
1020  /// See [TextEmphasisPosition](TextEmphasisPosition).
1021  pub enum TextEmphasisPositionVertical {
1022    /// Draw marks over the text in horizontal typographic modes.
1023    Over,
1024    /// Draw marks under the text in horizontal typographic modes.
1025    Under,
1026  }
1027}
1028
1029enum_property! {
1030  /// 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.
1031  ///
1032  /// See [TextEmphasisPosition](TextEmphasisPosition).
1033  pub enum TextEmphasisPositionHorizontal {
1034    /// Draw marks to the right of the text in vertical typographic modes.
1035    Left,
1036    /// Draw marks to the left of the text in vertical typographic modes.
1037    Right,
1038  }
1039}
1040
1041/// A value for the [text-emphasis-position](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-position-property) property.
1042#[derive(Debug, Clone, PartialEq)]
1043#[cfg_attr(feature = "visitor", derive(Visit))]
1044#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1045#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1046#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
1047pub struct TextEmphasisPosition {
1048  /// The vertical position.
1049  pub vertical: TextEmphasisPositionVertical,
1050  /// The horizontal position.
1051  pub horizontal: TextEmphasisPositionHorizontal,
1052}
1053
1054impl<'i> Parse<'i> for TextEmphasisPosition {
1055  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
1056    if let Ok(horizontal) = input.try_parse(TextEmphasisPositionHorizontal::parse) {
1057      let vertical = TextEmphasisPositionVertical::parse(input)?;
1058      Ok(TextEmphasisPosition { horizontal, vertical })
1059    } else {
1060      let vertical = TextEmphasisPositionVertical::parse(input)?;
1061      let horizontal = input
1062        .try_parse(TextEmphasisPositionHorizontal::parse)
1063        .unwrap_or(TextEmphasisPositionHorizontal::Right);
1064      Ok(TextEmphasisPosition { horizontal, vertical })
1065    }
1066  }
1067}
1068
1069enum_property! {
1070  /// A value for the [box-decoration-break](https://www.w3.org/TR/css-break-3/#break-decoration) property.
1071  pub enum BoxDecorationBreak {
1072    /// The element is rendered with no breaks present, and then sliced by the breaks afterward.
1073    Slice,
1074    /// Each box fragment is independently wrapped with the border, padding, and margin.
1075    Clone,
1076  }
1077}
1078
1079impl Default for BoxDecorationBreak {
1080  fn default() -> Self {
1081    BoxDecorationBreak::Slice
1082  }
1083}
1084
1085impl ToCss for TextEmphasisPosition {
1086  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
1087  where
1088    W: std::fmt::Write,
1089  {
1090    self.vertical.to_css(dest)?;
1091    if self.horizontal != TextEmphasisPositionHorizontal::Right {
1092      dest.write_char(' ')?;
1093      self.horizontal.to_css(dest)?;
1094    }
1095    Ok(())
1096  }
1097}
1098
1099#[derive(Default)]
1100pub(crate) struct TextDecorationHandler<'i> {
1101  line: Option<(TextDecorationLine, VendorPrefix)>,
1102  thickness: Option<TextDecorationThickness>,
1103  style: Option<(TextDecorationStyle, VendorPrefix)>,
1104  color: Option<(CssColor, VendorPrefix)>,
1105  emphasis_style: Option<(TextEmphasisStyle<'i>, VendorPrefix)>,
1106  emphasis_color: Option<(CssColor, VendorPrefix)>,
1107  emphasis_position: Option<(TextEmphasisPosition, VendorPrefix)>,
1108  has_any: bool,
1109}
1110
1111impl<'i> PropertyHandler<'i> for TextDecorationHandler<'i> {
1112  fn handle_property(
1113    &mut self,
1114    property: &Property<'i>,
1115    dest: &mut DeclarationList<'i>,
1116    context: &mut PropertyHandlerContext<'i, '_>,
1117  ) -> bool {
1118    use Property::*;
1119
1120    macro_rules! maybe_flush {
1121      ($prop: ident, $val: expr, $vp: expr) => {{
1122        // If two vendor prefixes for the same property have different
1123        // values, we need to flush what we have immediately to preserve order.
1124        if let Some((val, prefixes)) = &self.$prop {
1125          if val != $val && !prefixes.contains(*$vp) {
1126            self.finalize(dest, context);
1127          }
1128        }
1129      }};
1130    }
1131
1132    macro_rules! property {
1133      ($prop: ident, $val: expr, $vp: expr) => {{
1134        maybe_flush!($prop, $val, $vp);
1135
1136        // Otherwise, update the value and add the prefix.
1137        if let Some((val, prefixes)) = &mut self.$prop {
1138          *val = $val.clone();
1139          *prefixes |= *$vp;
1140        } else {
1141          self.$prop = Some(($val.clone(), *$vp));
1142          self.has_any = true;
1143        }
1144      }};
1145    }
1146
1147    match property {
1148      TextDecorationLine(val, vp) => property!(line, val, vp),
1149      TextDecorationThickness(val) => {
1150        self.thickness = Some(val.clone());
1151        self.has_any = true;
1152      }
1153      TextDecorationStyle(val, vp) => property!(style, val, vp),
1154      TextDecorationColor(val, vp) => property!(color, val, vp),
1155      TextDecoration(val, vp) => {
1156        maybe_flush!(line, &val.line, vp);
1157        maybe_flush!(style, &val.style, vp);
1158        maybe_flush!(color, &val.color, vp);
1159        property!(line, &val.line, vp);
1160        self.thickness = Some(val.thickness.clone());
1161        property!(style, &val.style, vp);
1162        property!(color, &val.color, vp);
1163      }
1164      TextEmphasisStyle(val, vp) => property!(emphasis_style, val, vp),
1165      TextEmphasisColor(val, vp) => property!(emphasis_color, val, vp),
1166      TextEmphasis(val, vp) => {
1167        maybe_flush!(emphasis_style, &val.style, vp);
1168        maybe_flush!(emphasis_color, &val.color, vp);
1169        property!(emphasis_style, &val.style, vp);
1170        property!(emphasis_color, &val.color, vp);
1171      }
1172      TextEmphasisPosition(val, vp) => property!(emphasis_position, val, vp),
1173      TextAlign(align) => {
1174        use super::text::*;
1175        macro_rules! logical {
1176          ($ltr: ident, $rtl: ident) => {{
1177            let logical_supported = !context.should_compile_logical(compat::Feature::LogicalTextAlign);
1178            if logical_supported {
1179              dest.push(property.clone());
1180            } else {
1181              context.add_logical_rule(
1182                Property::TextAlign(TextAlign::$ltr),
1183                Property::TextAlign(TextAlign::$rtl),
1184              );
1185            }
1186          }};
1187        }
1188
1189        match align {
1190          TextAlign::Start => logical!(Left, Right),
1191          TextAlign::End => logical!(Right, Left),
1192          _ => dest.push(property.clone()),
1193        }
1194      }
1195      Unparsed(val) if is_text_decoration_property(&val.property_id) => {
1196        self.finalize(dest, context);
1197        let mut unparsed = val.get_prefixed(context.targets, Feature::TextDecoration);
1198        context.add_unparsed_fallbacks(&mut unparsed);
1199        dest.push(Property::Unparsed(unparsed))
1200      }
1201      Unparsed(val) if is_text_emphasis_property(&val.property_id) => {
1202        self.finalize(dest, context);
1203        let mut unparsed = val.get_prefixed(context.targets, Feature::TextEmphasis);
1204        context.add_unparsed_fallbacks(&mut unparsed);
1205        dest.push(Property::Unparsed(unparsed))
1206      }
1207      _ => return false,
1208    }
1209
1210    true
1211  }
1212
1213  fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
1214    if !self.has_any {
1215      return;
1216    }
1217
1218    self.has_any = false;
1219
1220    let mut line = std::mem::take(&mut self.line);
1221    let mut thickness = std::mem::take(&mut self.thickness);
1222    let mut style = std::mem::take(&mut self.style);
1223    let mut color = std::mem::take(&mut self.color);
1224    let mut emphasis_style = std::mem::take(&mut self.emphasis_style);
1225    let mut emphasis_color = std::mem::take(&mut self.emphasis_color);
1226    let emphasis_position = std::mem::take(&mut self.emphasis_position);
1227
1228    if let (Some((line, line_vp)), Some(thickness_val), Some((style, style_vp)), Some((color, color_vp))) =
1229      (&mut line, &mut thickness, &mut style, &mut color)
1230    {
1231      let intersection = *line_vp | *style_vp | *color_vp;
1232      if !intersection.is_empty() {
1233        let mut prefix = intersection;
1234
1235        // Some browsers don't support thickness in the shorthand property yet.
1236        let supports_thickness = context.targets.is_compatible(compat::Feature::TextDecorationThicknessShorthand);
1237        let mut decoration = TextDecoration {
1238          line: line.clone(),
1239          thickness: if supports_thickness {
1240            thickness_val.clone()
1241          } else {
1242            TextDecorationThickness::default()
1243          },
1244          style: style.clone(),
1245          color: color.clone(),
1246        };
1247
1248        // Only add prefixes if one of the new sub-properties was used
1249        if prefix.contains(VendorPrefix::None)
1250          && (*style != TextDecorationStyle::default() || *color != CssColor::current_color())
1251        {
1252          prefix = context.targets.prefixes(VendorPrefix::None, Feature::TextDecoration);
1253
1254          let fallbacks = decoration.get_fallbacks(context.targets);
1255          for fallback in fallbacks {
1256            dest.push(Property::TextDecoration(fallback, prefix))
1257          }
1258        }
1259
1260        dest.push(Property::TextDecoration(decoration, prefix));
1261        line_vp.remove(intersection);
1262        style_vp.remove(intersection);
1263        color_vp.remove(intersection);
1264        if supports_thickness || *thickness_val == TextDecorationThickness::default() {
1265          thickness = None;
1266        }
1267      }
1268    }
1269
1270    macro_rules! color {
1271      ($key: ident, $prop: ident) => {
1272        if let Some((mut val, vp)) = $key {
1273          if !vp.is_empty() {
1274            let prefix = context.targets.prefixes(vp, Feature::$prop);
1275            if prefix.contains(VendorPrefix::None) {
1276              let fallbacks = val.get_fallbacks(context.targets);
1277              for fallback in fallbacks {
1278                dest.push(Property::$prop(fallback, prefix))
1279              }
1280            }
1281            dest.push(Property::$prop(val, prefix))
1282          }
1283        }
1284      };
1285    }
1286
1287    macro_rules! single_property {
1288      ($key: ident, $prop: ident) => {
1289        if let Some((val, vp)) = $key {
1290          if !vp.is_empty() {
1291            let prefix = context.targets.prefixes(vp, Feature::$prop);
1292            dest.push(Property::$prop(val, prefix))
1293          }
1294        }
1295      };
1296    }
1297
1298    single_property!(line, TextDecorationLine);
1299    single_property!(style, TextDecorationStyle);
1300    color!(color, TextDecorationColor);
1301
1302    if let Some(thickness) = thickness {
1303      // Percentages in the text-decoration-thickness property are based on 1em.
1304      // If unsupported, compile this to a calc() instead.
1305      match thickness {
1306        TextDecorationThickness::LengthPercentage(LengthPercentage::Percentage(p))
1307          if should_compile!(context.targets, TextDecorationThicknessPercent) =>
1308        {
1309          let calc = Calc::Function(Box::new(MathFunction::Calc(Calc::Product(
1310            p.0,
1311            Box::new(Calc::Value(Box::new(LengthPercentage::Dimension(LengthValue::Em(1.0))))),
1312          ))));
1313          let thickness = TextDecorationThickness::LengthPercentage(LengthPercentage::Calc(Box::new(calc)));
1314          dest.push(Property::TextDecorationThickness(thickness));
1315        }
1316        thickness => dest.push(Property::TextDecorationThickness(thickness)),
1317      }
1318    }
1319
1320    if let (Some((style, style_vp)), Some((color, color_vp))) = (&mut emphasis_style, &mut emphasis_color) {
1321      let intersection = *style_vp | *color_vp;
1322      if !intersection.is_empty() {
1323        let prefix = context.targets.prefixes(intersection, Feature::TextEmphasis);
1324        let mut emphasis = TextEmphasis {
1325          style: style.clone(),
1326          color: color.clone(),
1327        };
1328
1329        if prefix.contains(VendorPrefix::None) {
1330          let fallbacks = emphasis.get_fallbacks(context.targets);
1331          for fallback in fallbacks {
1332            dest.push(Property::TextEmphasis(fallback, prefix))
1333          }
1334        }
1335
1336        dest.push(Property::TextEmphasis(emphasis, prefix));
1337        style_vp.remove(intersection);
1338        color_vp.remove(intersection);
1339      }
1340    }
1341
1342    single_property!(emphasis_style, TextEmphasisStyle);
1343    color!(emphasis_color, TextEmphasisColor);
1344
1345    if let Some((pos, vp)) = emphasis_position {
1346      if !vp.is_empty() {
1347        let mut prefix = context.targets.prefixes(vp, Feature::TextEmphasisPosition);
1348        // Prefixed version does not support horizontal keyword.
1349        if pos.horizontal != TextEmphasisPositionHorizontal::Right {
1350          prefix = VendorPrefix::None;
1351        }
1352        dest.push(Property::TextEmphasisPosition(pos, prefix))
1353      }
1354    }
1355  }
1356}
1357
1358/// A value for the [text-shadow](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-shadow-property) property.
1359#[derive(Debug, Clone, PartialEq)]
1360#[cfg_attr(feature = "visitor", derive(Visit))]
1361#[cfg_attr(
1362  feature = "serde",
1363  derive(serde::Serialize, serde::Deserialize),
1364  serde(rename_all = "camelCase")
1365)]
1366#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1367#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
1368pub struct TextShadow {
1369  /// The color of the text shadow.
1370  pub color: CssColor,
1371  /// The x offset of the text shadow.
1372  pub x_offset: Length,
1373  /// The y offset of the text shadow.
1374  pub y_offset: Length,
1375  /// The blur radius of the text shadow.
1376  pub blur: Length,
1377  /// The spread distance of the text shadow.
1378  pub spread: Length, // added in Level 4 spec
1379}
1380
1381impl<'i> Parse<'i> for TextShadow {
1382  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
1383    let mut color = None;
1384    let mut lengths = None;
1385
1386    loop {
1387      if lengths.is_none() {
1388        let value = input.try_parse::<_, _, ParseError<ParserError<'i>>>(|input| {
1389          let horizontal = Length::parse(input)?;
1390          let vertical = Length::parse(input)?;
1391          let blur = input.try_parse(Length::parse).unwrap_or(Length::zero());
1392          let spread = input.try_parse(Length::parse).unwrap_or(Length::zero());
1393          Ok((horizontal, vertical, blur, spread))
1394        });
1395
1396        if let Ok(value) = value {
1397          lengths = Some(value);
1398          continue;
1399        }
1400      }
1401
1402      if color.is_none() {
1403        if let Ok(value) = input.try_parse(CssColor::parse) {
1404          color = Some(value);
1405          continue;
1406        }
1407      }
1408
1409      break;
1410    }
1411
1412    let lengths = lengths.ok_or(input.new_error(BasicParseErrorKind::QualifiedRuleInvalid))?;
1413    Ok(TextShadow {
1414      color: color.unwrap_or(CssColor::current_color()),
1415      x_offset: lengths.0,
1416      y_offset: lengths.1,
1417      blur: lengths.2,
1418      spread: lengths.3,
1419    })
1420  }
1421}
1422
1423impl ToCss for TextShadow {
1424  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
1425  where
1426    W: std::fmt::Write,
1427  {
1428    self.x_offset.to_css(dest)?;
1429    dest.write_char(' ')?;
1430    self.y_offset.to_css(dest)?;
1431
1432    if self.blur != Length::zero() || self.spread != Length::zero() {
1433      dest.write_char(' ')?;
1434      self.blur.to_css(dest)?;
1435
1436      if self.spread != Length::zero() {
1437        dest.write_char(' ')?;
1438        self.spread.to_css(dest)?;
1439      }
1440    }
1441
1442    if self.color != CssColor::current_color() {
1443      dest.write_char(' ')?;
1444      self.color.to_css(dest)?;
1445    }
1446
1447    Ok(())
1448  }
1449}
1450
1451impl IsCompatible for TextShadow {
1452  fn is_compatible(&self, browsers: Browsers) -> bool {
1453    self.color.is_compatible(browsers)
1454      && self.x_offset.is_compatible(browsers)
1455      && self.y_offset.is_compatible(browsers)
1456      && self.blur.is_compatible(browsers)
1457      && self.spread.is_compatible(browsers)
1458  }
1459}
1460
1461#[inline]
1462fn is_text_decoration_property(property_id: &PropertyId) -> bool {
1463  match property_id {
1464    PropertyId::TextDecorationLine(_)
1465    | PropertyId::TextDecorationThickness
1466    | PropertyId::TextDecorationStyle(_)
1467    | PropertyId::TextDecorationColor(_)
1468    | PropertyId::TextDecoration(_) => true,
1469    _ => false,
1470  }
1471}
1472
1473#[inline]
1474fn is_text_emphasis_property(property_id: &PropertyId) -> bool {
1475  match property_id {
1476    PropertyId::TextEmphasisStyle(_)
1477    | PropertyId::TextEmphasisColor(_)
1478    | PropertyId::TextEmphasis(_)
1479    | PropertyId::TextEmphasisPosition(_) => true,
1480    _ => false,
1481  }
1482}
1483
1484impl FallbackValues for SmallVec<[TextShadow; 1]> {
1485  fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
1486    let mut fallbacks = ColorFallbackKind::empty();
1487    for shadow in self.iter() {
1488      fallbacks |= shadow.color.get_necessary_fallbacks(targets);
1489    }
1490
1491    let mut res = Vec::new();
1492    if fallbacks.contains(ColorFallbackKind::RGB) {
1493      let rgb = self
1494        .iter()
1495        .map(|shadow| TextShadow {
1496          color: shadow.color.to_rgb().unwrap(),
1497          ..shadow.clone()
1498        })
1499        .collect();
1500      res.push(rgb);
1501    }
1502
1503    if fallbacks.contains(ColorFallbackKind::P3) {
1504      let p3 = self
1505        .iter()
1506        .map(|shadow| TextShadow {
1507          color: shadow.color.to_p3().unwrap(),
1508          ..shadow.clone()
1509        })
1510        .collect();
1511      res.push(p3);
1512    }
1513
1514    if fallbacks.contains(ColorFallbackKind::LAB) {
1515      for shadow in self.iter_mut() {
1516        shadow.color = shadow.color.to_lab().unwrap();
1517      }
1518    }
1519
1520    res
1521  }
1522}
1523
1524enum_property! {
1525  /// A value for the [direction](https://drafts.csswg.org/css-writing-modes-3/#direction) property.
1526  pub enum Direction {
1527    /// This value sets inline base direction (bidi directionality) to line-left-to-line-right.
1528    Ltr,
1529    /// This value sets inline base direction (bidi directionality) to line-right-to-line-left.
1530    Rtl,
1531  }
1532}
1533
1534enum_property! {
1535  /// A value for the [unicode-bidi](https://drafts.csswg.org/css-writing-modes-3/#unicode-bidi) property.
1536  pub enum UnicodeBidi {
1537    /// The box does not open an additional level of embedding.
1538    Normal,
1539    /// If the box is inline, this value creates a directional embedding by opening an additional level of embedding.
1540    Embed,
1541    /// On an inline box, this bidi-isolates its contents.
1542    Isolate,
1543    /// This value puts the box’s immediate inline content in a directional override.
1544    BidiOverride,
1545    /// This combines the isolation behavior of isolate with the directional override behavior of bidi-override.
1546    IsolateOverride,
1547    /// This value behaves as isolate except that the base directionality is determined using a heuristic rather than the direction property.
1548    Plaintext,
1549  }
1550}