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