Skip to main content

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