lightningcss/values/
syntax.rs

1//! CSS syntax strings
2
3use super::ident::Ident;
4use super::number::{CSSInteger, CSSNumber};
5use crate::error::{ParserError, PrinterError};
6use crate::printer::Printer;
7use crate::properties::custom::TokenList;
8use crate::stylesheet::ParserOptions;
9use crate::traits::{Parse, ToCss};
10use crate::values;
11#[cfg(feature = "visitor")]
12use crate::visitor::Visit;
13use cssparser::*;
14
15/// A CSS [syntax string](https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings)
16/// used to define the grammar for a registered custom property.
17#[derive(Debug, PartialEq, Clone)]
18#[cfg_attr(
19  feature = "serde",
20  derive(serde::Serialize, serde::Deserialize),
21  serde(tag = "type", content = "value", rename_all = "kebab-case")
22)]
23#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
24#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
25pub enum SyntaxString {
26  /// A list of syntax components.
27  Components(Vec<SyntaxComponent>),
28  /// The universal syntax definition.
29  Universal,
30}
31
32/// A [syntax component](https://drafts.css-houdini.org/css-properties-values-api/#syntax-component)
33/// within a [SyntaxString](SyntaxString).
34///
35/// A syntax component consists of a component kind an a multiplier, which indicates how the component
36/// may repeat during parsing.
37#[derive(Debug, PartialEq, Clone)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
40#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
41pub struct SyntaxComponent {
42  /// The kind of component.
43  pub kind: SyntaxComponentKind,
44  /// A multiplier for the component.
45  pub multiplier: Multiplier,
46}
47
48/// A [syntax component component name](https://drafts.css-houdini.org/css-properties-values-api/#supported-names).
49#[derive(Debug, PartialEq, Clone)]
50#[cfg_attr(
51  feature = "serde",
52  derive(serde::Serialize, serde::Deserialize),
53  serde(tag = "type", content = "value", rename_all = "kebab-case")
54)]
55#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
56#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
57pub enum SyntaxComponentKind {
58  /// A `<length>` component.
59  Length,
60  /// A `<number>` component.
61  Number,
62  /// A `<percentage>` component.
63  Percentage,
64  /// A `<length-percentage>` component.
65  LengthPercentage,
66  /// A `<color>` component.
67  Color,
68  /// An `<image>` component.
69  Image,
70  /// A `<url>` component.
71  Url,
72  /// An `<integer>` component.
73  Integer,
74  /// An `<angle>` component.
75  Angle,
76  /// A `<time>` component.
77  Time,
78  /// A `<resolution>` component.
79  Resolution,
80  /// A `<transform-function>` component.
81  TransformFunction,
82  /// A `<transform-list>` component.
83  TransformList,
84  /// A `<custom-ident>` component.
85  CustomIdent,
86  /// A literal component.
87  Literal(String), // TODO: borrow??
88}
89
90/// A [multiplier](https://drafts.css-houdini.org/css-properties-values-api/#multipliers) for a
91/// [SyntaxComponent](SyntaxComponent). Indicates whether and how the component may be repeated.
92#[derive(Debug, PartialEq, Clone)]
93#[cfg_attr(feature = "visitor", derive(Visit))]
94#[cfg_attr(
95  feature = "serde",
96  derive(serde::Serialize, serde::Deserialize),
97  serde(tag = "type", content = "value", rename_all = "kebab-case")
98)]
99#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
100#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
101pub enum Multiplier {
102  /// The component may not be repeated.
103  None,
104  /// The component may repeat one or more times, separated by spaces.
105  Space,
106  /// The component may repeat one or more times, separated by commas.
107  Comma,
108}
109
110/// A parsed value for a [SyntaxComponent](SyntaxComponent).
111#[derive(Debug, PartialEq, Clone)]
112#[cfg_attr(feature = "visitor", derive(Visit))]
113#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
114#[cfg_attr(
115  feature = "serde",
116  derive(serde::Serialize, serde::Deserialize),
117  serde(tag = "type", content = "value", rename_all = "kebab-case")
118)]
119#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
120pub enum ParsedComponent<'i> {
121  /// A `<length>` value.
122  Length(values::length::Length),
123  /// A `<number>` value.
124  Number(CSSNumber),
125  /// A `<percentage>` value.
126  Percentage(values::percentage::Percentage),
127  /// A `<length-percentage>` value.
128  LengthPercentage(values::length::LengthPercentage),
129  /// A `<color>` value.
130  Color(values::color::CssColor),
131  /// An `<image>` value.
132  #[cfg_attr(feature = "serde", serde(borrow))]
133  Image(values::image::Image<'i>),
134  /// A `<url>` value.
135  Url(values::url::Url<'i>),
136  /// An `<integer>` value.
137  Integer(CSSInteger),
138  /// An `<angle>` value.
139  Angle(values::angle::Angle),
140  /// A `<time>` value.
141  Time(values::time::Time),
142  /// A `<resolution>` value.
143  Resolution(values::resolution::Resolution),
144  /// A `<transform-function>` value.
145  TransformFunction(crate::properties::transform::Transform),
146  /// A `<transform-list>` value.
147  TransformList(crate::properties::transform::TransformList),
148  /// A `<custom-ident>` value.
149  CustomIdent(values::ident::CustomIdent<'i>),
150  /// A literal value.
151  Literal(Ident<'i>),
152  /// A repeated component value.
153  Repeated {
154    /// The components to repeat.
155    #[cfg_attr(feature = "visitor", skip_type)]
156    components: Vec<ParsedComponent<'i>>,
157    /// A multiplier describing how the components repeat.
158    multiplier: Multiplier,
159  },
160  /// A raw token stream.
161  TokenList(crate::properties::custom::TokenList<'i>),
162}
163
164impl<'i> SyntaxString {
165  /// Parses a syntax string.
166  pub fn parse_string(input: &'i str) -> Result<SyntaxString, ()> {
167    // https://drafts.css-houdini.org/css-properties-values-api/#parsing-syntax
168    let mut input = input.trim_matches(SPACE_CHARACTERS);
169    if input.is_empty() {
170      return Err(());
171    }
172
173    if input == "*" {
174      return Ok(SyntaxString::Universal);
175    }
176
177    let mut components = Vec::new();
178    loop {
179      let component = SyntaxComponent::parse_string(&mut input)?;
180      components.push(component);
181
182      input = input.trim_start_matches(SPACE_CHARACTERS);
183      if input.is_empty() {
184        break;
185      }
186
187      if input.starts_with('|') {
188        input = &input[1..];
189        continue;
190      }
191
192      return Err(());
193    }
194
195    Ok(SyntaxString::Components(components))
196  }
197
198  /// Parses a value according to the syntax grammar.
199  pub fn parse_value<'t>(
200    &self,
201    input: &mut Parser<'i, 't>,
202  ) -> Result<ParsedComponent<'i>, ParseError<'i, ParserError<'i>>> {
203    match self {
204      SyntaxString::Universal => Ok(ParsedComponent::TokenList(TokenList::parse(
205        input,
206        &ParserOptions::default(),
207        0,
208      )?)),
209      SyntaxString::Components(components) => {
210        // Loop through each component, and return the first one that parses successfully.
211        for component in components {
212          let state = input.state();
213          let mut parsed = Vec::new();
214          loop {
215            let value: Result<ParsedComponent<'i>, ParseError<'i, ParserError<'i>>> = input.try_parse(|input| {
216              Ok(match &component.kind {
217                SyntaxComponentKind::Length => ParsedComponent::Length(values::length::Length::parse(input)?),
218                SyntaxComponentKind::Number => ParsedComponent::Number(CSSNumber::parse(input)?),
219                SyntaxComponentKind::Percentage => {
220                  ParsedComponent::Percentage(values::percentage::Percentage::parse(input)?)
221                }
222                SyntaxComponentKind::LengthPercentage => {
223                  ParsedComponent::LengthPercentage(values::length::LengthPercentage::parse(input)?)
224                }
225                SyntaxComponentKind::Color => ParsedComponent::Color(values::color::CssColor::parse(input)?),
226                SyntaxComponentKind::Image => ParsedComponent::Image(values::image::Image::parse(input)?),
227                SyntaxComponentKind::Url => ParsedComponent::Url(values::url::Url::parse(input)?),
228                SyntaxComponentKind::Integer => ParsedComponent::Integer(CSSInteger::parse(input)?),
229                SyntaxComponentKind::Angle => ParsedComponent::Angle(values::angle::Angle::parse(input)?),
230                SyntaxComponentKind::Time => ParsedComponent::Time(values::time::Time::parse(input)?),
231                SyntaxComponentKind::Resolution => {
232                  ParsedComponent::Resolution(values::resolution::Resolution::parse(input)?)
233                }
234                SyntaxComponentKind::TransformFunction => {
235                  ParsedComponent::TransformFunction(crate::properties::transform::Transform::parse(input)?)
236                }
237                SyntaxComponentKind::TransformList => {
238                  ParsedComponent::TransformList(crate::properties::transform::TransformList::parse(input)?)
239                }
240                SyntaxComponentKind::CustomIdent => {
241                  ParsedComponent::CustomIdent(values::ident::CustomIdent::parse(input)?)
242                }
243                SyntaxComponentKind::Literal(value) => {
244                  let location = input.current_source_location();
245                  let ident = input.expect_ident()?;
246                  if *ident != &value {
247                    return Err(location.new_unexpected_token_error(Token::Ident(ident.clone())));
248                  }
249                  ParsedComponent::Literal(ident.into())
250                }
251              })
252            });
253
254            if let Ok(value) = value {
255              match component.multiplier {
256                Multiplier::None => return Ok(value),
257                Multiplier::Space => {
258                  parsed.push(value);
259                  if input.is_exhausted() {
260                    return Ok(ParsedComponent::Repeated {
261                      components: parsed,
262                      multiplier: component.multiplier.clone(),
263                    });
264                  }
265                }
266                Multiplier::Comma => {
267                  parsed.push(value);
268                  match input.next() {
269                    Err(_) => {
270                      return Ok(ParsedComponent::Repeated {
271                        components: parsed,
272                        multiplier: component.multiplier.clone(),
273                      })
274                    }
275                    Ok(&Token::Comma) => continue,
276                    Ok(_) => break,
277                  }
278                }
279              }
280            } else {
281              break;
282            }
283          }
284
285          input.reset(&state);
286        }
287
288        Err(input.new_error_for_next_token())
289      }
290    }
291  }
292
293  /// Parses a value from a string according to the syntax grammar.
294  pub fn parse_value_from_string<'t>(
295    &self,
296    input: &'i str,
297  ) -> Result<ParsedComponent<'i>, ParseError<'i, ParserError<'i>>> {
298    let mut input = ParserInput::new(input);
299    let mut parser = Parser::new(&mut input);
300    self.parse_value(&mut parser)
301  }
302}
303
304impl SyntaxComponent {
305  fn parse_string(input: &mut &str) -> Result<SyntaxComponent, ()> {
306    let kind = SyntaxComponentKind::parse_string(input)?;
307
308    // Pre-multiplied types cannot have multipliers.
309    if kind == SyntaxComponentKind::TransformList {
310      return Ok(SyntaxComponent {
311        kind,
312        multiplier: Multiplier::None,
313      });
314    }
315
316    let multiplier = if input.starts_with('+') {
317      *input = &input[1..];
318      Multiplier::Space
319    } else if input.starts_with('#') {
320      *input = &input[1..];
321      Multiplier::Comma
322    } else {
323      Multiplier::None
324    };
325
326    Ok(SyntaxComponent { kind, multiplier })
327  }
328}
329
330// https://drafts.csswg.org/css-syntax-3/#whitespace
331static SPACE_CHARACTERS: &'static [char] = &['\u{0020}', '\u{0009}'];
332
333impl SyntaxComponentKind {
334  fn parse_string(input: &mut &str) -> Result<SyntaxComponentKind, ()> {
335    // https://drafts.css-houdini.org/css-properties-values-api/#consume-syntax-component
336    *input = input.trim_start_matches(SPACE_CHARACTERS);
337    if input.starts_with('<') {
338      // https://drafts.css-houdini.org/css-properties-values-api/#consume-data-type-name
339      let end_idx = input.find('>').ok_or(())?;
340      let name = &input[1..end_idx];
341      let component = match_ignore_ascii_case! {name,
342        "length" => SyntaxComponentKind::Length,
343        "number" => SyntaxComponentKind::Number,
344        "percentage" => SyntaxComponentKind::Percentage,
345        "length-percentage" => SyntaxComponentKind::LengthPercentage,
346        "color" => SyntaxComponentKind::Color,
347        "image" => SyntaxComponentKind::Image,
348        "url" => SyntaxComponentKind::Url,
349        "integer" => SyntaxComponentKind::Integer,
350        "angle" => SyntaxComponentKind::Angle,
351        "time" => SyntaxComponentKind::Time,
352        "resolution" => SyntaxComponentKind::Resolution,
353        "transform-function" => SyntaxComponentKind::TransformFunction,
354        "transform-list" => SyntaxComponentKind::TransformList,
355        "custom-ident" => SyntaxComponentKind::CustomIdent,
356        _ => return Err(())
357      };
358
359      *input = &input[end_idx + 1..];
360      Ok(component)
361    } else if input.starts_with(is_ident_start) {
362      // A literal.
363      let end_idx = input.find(|c| !is_name_code_point(c)).unwrap_or_else(|| input.len());
364      let name = input[0..end_idx].to_owned();
365      *input = &input[end_idx..];
366      Ok(SyntaxComponentKind::Literal(name))
367    } else {
368      return Err(());
369    }
370  }
371}
372
373#[inline]
374fn is_ident_start(c: char) -> bool {
375  // https://drafts.csswg.org/css-syntax-3/#ident-start-code-point
376  c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '\u{80}' || c == '_'
377}
378
379#[inline]
380fn is_name_code_point(c: char) -> bool {
381  // https://drafts.csswg.org/css-syntax-3/#ident-code-point
382  is_ident_start(c) || c >= '0' && c <= '9' || c == '-'
383}
384
385impl<'i> Parse<'i> for SyntaxString {
386  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
387    let string = input.expect_string_cloned()?;
388    SyntaxString::parse_string(string.as_ref()).map_err(|_| input.new_custom_error(ParserError::InvalidValue))
389  }
390}
391
392impl ToCss for SyntaxString {
393  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
394  where
395    W: std::fmt::Write,
396  {
397    dest.write_char('"')?;
398    match self {
399      SyntaxString::Universal => dest.write_char('*')?,
400      SyntaxString::Components(components) => {
401        let mut first = true;
402        for component in components {
403          if first {
404            first = false;
405          } else {
406            dest.delim('|', true)?;
407          }
408
409          component.to_css(dest)?;
410        }
411      }
412    }
413
414    dest.write_char('"')
415  }
416}
417
418impl ToCss for SyntaxComponent {
419  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
420  where
421    W: std::fmt::Write,
422  {
423    self.kind.to_css(dest)?;
424    match self.multiplier {
425      Multiplier::None => Ok(()),
426      Multiplier::Comma => dest.write_char('#'),
427      Multiplier::Space => dest.write_char('+'),
428    }
429  }
430}
431
432impl ToCss for SyntaxComponentKind {
433  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
434  where
435    W: std::fmt::Write,
436  {
437    use SyntaxComponentKind::*;
438    let s = match self {
439      Length => "<length>",
440      Number => "<number>",
441      Percentage => "<percentage>",
442      LengthPercentage => "<length-percentage>",
443      Color => "<color>",
444      Image => "<image>",
445      Url => "<url>",
446      Integer => "<integer>",
447      Angle => "<angle>",
448      Time => "<time>",
449      Resolution => "<resolution>",
450      TransformFunction => "<transform-function>",
451      TransformList => "<transform-list>",
452      CustomIdent => "<custom-ident>",
453      Literal(l) => l,
454    };
455    dest.write_str(s)
456  }
457}
458
459impl<'i> ToCss for ParsedComponent<'i> {
460  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
461  where
462    W: std::fmt::Write,
463  {
464    use ParsedComponent::*;
465    match self {
466      Length(v) => v.to_css(dest),
467      Number(v) => v.to_css(dest),
468      Percentage(v) => v.to_css(dest),
469      LengthPercentage(v) => v.to_css(dest),
470      Color(v) => v.to_css(dest),
471      Image(v) => v.to_css(dest),
472      Url(v) => v.to_css(dest),
473      Integer(v) => v.to_css(dest),
474      Angle(v) => v.to_css(dest),
475      Time(v) => v.to_css(dest),
476      Resolution(v) => v.to_css(dest),
477      TransformFunction(v) => v.to_css(dest),
478      TransformList(v) => v.to_css(dest),
479      CustomIdent(v) => v.to_css(dest),
480      Literal(v) => v.to_css(dest),
481      Repeated { components, multiplier } => {
482        let mut first = true;
483        for component in components {
484          if first {
485            first = false;
486          } else {
487            match multiplier {
488              Multiplier::Comma => dest.delim(',', false)?,
489              Multiplier::Space => dest.write_char(' ')?,
490              Multiplier::None => unreachable!(),
491            }
492          }
493
494          component.to_css(dest)?;
495        }
496        Ok(())
497      }
498      TokenList(t) => t.to_css(dest, false),
499    }
500  }
501}
502
503#[cfg(test)]
504mod tests {
505  use crate::values::color::RGBA;
506
507  use super::*;
508
509  fn test(source: &str, test: &str, expected: ParsedComponent) {
510    let parsed = SyntaxString::parse_string(source).unwrap();
511
512    let mut input = ParserInput::new(test);
513    let mut parser = Parser::new(&mut input);
514    let value = parsed.parse_value(&mut parser).unwrap();
515    assert_eq!(value, expected);
516  }
517
518  fn parse_error_test(source: &str) {
519    let res = SyntaxString::parse_string(source);
520    match res {
521      Ok(_) => unreachable!(),
522      Err(_) => {}
523    }
524  }
525
526  fn error_test(source: &str, test: &str) {
527    let parsed = SyntaxString::parse_string(source).unwrap();
528    let mut input = ParserInput::new(test);
529    let mut parser = Parser::new(&mut input);
530    let res = parsed.parse_value(&mut parser);
531    match res {
532      Ok(_) => unreachable!(),
533      Err(_) => {}
534    }
535  }
536
537  #[test]
538  fn test_syntax() {
539    test(
540      "foo | <color>+ | <integer>",
541      "foo",
542      ParsedComponent::Literal("foo".into()),
543    );
544
545    test("foo|<color>+|<integer>", "foo", ParsedComponent::Literal("foo".into()));
546
547    test("foo | <color>+ | <integer>", "2", ParsedComponent::Integer(2));
548
549    test(
550      "foo | <color>+ | <integer>",
551      "red",
552      ParsedComponent::Repeated {
553        components: vec![ParsedComponent::Color(values::color::CssColor::RGBA(RGBA {
554          red: 255,
555          green: 0,
556          blue: 0,
557          alpha: 255,
558        }))],
559        multiplier: Multiplier::Space,
560      },
561    );
562
563    test(
564      "foo | <color>+ | <integer>",
565      "red blue",
566      ParsedComponent::Repeated {
567        components: vec![
568          ParsedComponent::Color(values::color::CssColor::RGBA(RGBA {
569            red: 255,
570            green: 0,
571            blue: 0,
572            alpha: 255,
573          })),
574          ParsedComponent::Color(values::color::CssColor::RGBA(RGBA {
575            red: 0,
576            green: 0,
577            blue: 255,
578            alpha: 255,
579          })),
580        ],
581        multiplier: Multiplier::Space,
582      },
583    );
584
585    error_test("foo | <color>+ | <integer>", "2.5");
586
587    error_test("foo | <color>+ | <integer>", "25px");
588
589    error_test("foo | <color>+ | <integer>", "red, green");
590
591    test(
592      "foo | <color># | <integer>",
593      "red, blue",
594      ParsedComponent::Repeated {
595        components: vec![
596          ParsedComponent::Color(values::color::CssColor::RGBA(RGBA {
597            red: 255,
598            green: 0,
599            blue: 0,
600            alpha: 255,
601          })),
602          ParsedComponent::Color(values::color::CssColor::RGBA(RGBA {
603            red: 0,
604            green: 0,
605            blue: 255,
606            alpha: 255,
607          })),
608        ],
609        multiplier: Multiplier::Comma,
610      },
611    );
612
613    error_test("foo | <color># | <integer>", "red green");
614
615    test(
616      "<length>",
617      "25px",
618      ParsedComponent::Length(values::length::Length::Value(values::length::LengthValue::Px(25.0))),
619    );
620
621    test(
622      "<length>",
623      "calc(25px + 25px)",
624      ParsedComponent::Length(values::length::Length::Value(values::length::LengthValue::Px(50.0))),
625    );
626
627    test(
628      "<length> | <percentage>",
629      "25px",
630      ParsedComponent::Length(values::length::Length::Value(values::length::LengthValue::Px(25.0))),
631    );
632
633    test(
634      "<length> | <percentage>",
635      "25%",
636      ParsedComponent::Percentage(values::percentage::Percentage(0.25)),
637    );
638
639    error_test("<length> | <percentage>", "calc(100% - 25px)");
640
641    test("foo | bar | baz", "bar", ParsedComponent::Literal("bar".into()));
642
643    test(
644      "<custom-ident>",
645      "hi",
646      ParsedComponent::CustomIdent(values::ident::CustomIdent("hi".into())),
647    );
648
649    parse_error_test("<transform-list>#");
650    parse_error_test("<color");
651    parse_error_test("color>");
652  }
653}