blob: 42dfe485ce85bbdd80caa5e1d256bafcb7cbb06f [file] [log] [blame]
//! CSS syntax strings
use super::ident::Ident;
use super::number::{CSSInteger, CSSNumber};
use crate::error::{ParserError, PrinterError};
use crate::printer::Printer;
use crate::properties::custom::TokenList;
use crate::stylesheet::ParserOptions;
use crate::traits::{Parse, ToCss};
use crate::values;
#[cfg(feature = "visitor")]
use crate::visitor::Visit;
use cssparser::*;
/// A CSS [syntax string](https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings)
/// used to define the grammar for a registered custom property.
#[derive(Debug, PartialEq, Clone)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type", content = "value", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub enum SyntaxString {
/// A list of syntax components.
Components(Vec<SyntaxComponent>),
/// The universal syntax definition.
Universal,
}
/// A [syntax component](https://drafts.css-houdini.org/css-properties-values-api/#syntax-component)
/// within a [SyntaxString](SyntaxString).
///
/// A syntax component consists of a component kind an a multiplier, which indicates how the component
/// may repeat during parsing.
#[derive(Debug, PartialEq, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub struct SyntaxComponent {
/// The kind of component.
pub kind: SyntaxComponentKind,
/// A multiplier for the component.
pub multiplier: Multiplier,
}
/// A [syntax component component name](https://drafts.css-houdini.org/css-properties-values-api/#supported-names).
#[derive(Debug, PartialEq, Clone)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type", content = "value", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub enum SyntaxComponentKind {
/// A `<length>` component.
Length,
/// A `<number>` component.
Number,
/// A `<percentage>` component.
Percentage,
/// A `<length-percentage>` component.
LengthPercentage,
/// A `<color>` component.
Color,
/// An `<image>` component.
Image,
/// A `<url>` component.
Url,
/// An `<integer>` component.
Integer,
/// An `<angle>` component.
Angle,
/// A `<time>` component.
Time,
/// A `<resolution>` component.
Resolution,
/// A `<transform-function>` component.
TransformFunction,
/// A `<transform-list>` component.
TransformList,
/// A `<custom-ident>` component.
CustomIdent,
/// A literal component.
Literal(String), // TODO: borrow??
}
/// A [multiplier](https://drafts.css-houdini.org/css-properties-values-api/#multipliers) for a
/// [SyntaxComponent](SyntaxComponent). Indicates whether and how the component may be repeated.
#[derive(Debug, PartialEq, Clone)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type", content = "value", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub enum Multiplier {
/// The component may not be repeated.
None,
/// The component may repeat one or more times, separated by spaces.
Space,
/// The component may repeat one or more times, separated by commas.
Comma,
}
/// A parsed value for a [SyntaxComponent](SyntaxComponent).
#[derive(Debug, PartialEq, Clone)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type", content = "value", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub enum ParsedComponent<'i> {
/// A `<length>` value.
Length(values::length::Length),
/// A `<number>` value.
Number(CSSNumber),
/// A `<percentage>` value.
Percentage(values::percentage::Percentage),
/// A `<length-percentage>` value.
LengthPercentage(values::length::LengthPercentage),
/// A `<color>` value.
Color(values::color::CssColor),
/// An `<image>` value.
#[cfg_attr(feature = "serde", serde(borrow))]
Image(values::image::Image<'i>),
/// A `<url>` value.
Url(values::url::Url<'i>),
/// An `<integer>` value.
Integer(CSSInteger),
/// An `<angle>` value.
Angle(values::angle::Angle),
/// A `<time>` value.
Time(values::time::Time),
/// A `<resolution>` value.
Resolution(values::resolution::Resolution),
/// A `<transform-function>` value.
TransformFunction(crate::properties::transform::Transform),
/// A `<transform-list>` value.
TransformList(crate::properties::transform::TransformList),
/// A `<custom-ident>` value.
CustomIdent(values::ident::CustomIdent<'i>),
/// A literal value.
Literal(Ident<'i>),
/// A repeated component value.
Repeated {
/// The components to repeat.
#[cfg_attr(feature = "visitor", skip_type)]
components: Vec<ParsedComponent<'i>>,
/// A multiplier describing how the components repeat.
multiplier: Multiplier,
},
/// A raw token stream.
TokenList(crate::properties::custom::TokenList<'i>),
}
impl<'i> SyntaxString {
/// Parses a syntax string.
pub fn parse_string(input: &'i str) -> Result<SyntaxString, ()> {
// https://drafts.css-houdini.org/css-properties-values-api/#parsing-syntax
let mut input = input.trim_matches(SPACE_CHARACTERS);
if input.is_empty() {
return Err(());
}
if input == "*" {
return Ok(SyntaxString::Universal);
}
let mut components = Vec::new();
loop {
let component = SyntaxComponent::parse_string(&mut input)?;
components.push(component);
input = input.trim_start_matches(SPACE_CHARACTERS);
if input.is_empty() {
break;
}
if input.starts_with('|') {
input = &input[1..];
continue;
}
return Err(());
}
Ok(SyntaxString::Components(components))
}
/// Parses a value according to the syntax grammar.
pub fn parse_value<'t>(
&self,
input: &mut Parser<'i, 't>,
) -> Result<ParsedComponent<'i>, ParseError<'i, ParserError<'i>>> {
match self {
SyntaxString::Universal => Ok(ParsedComponent::TokenList(TokenList::parse(
input,
&ParserOptions::default(),
0,
)?)),
SyntaxString::Components(components) => {
// Loop through each component, and return the first one that parses successfully.
for component in components {
let state = input.state();
let mut parsed = Vec::new();
loop {
let value: Result<ParsedComponent<'i>, ParseError<'i, ParserError<'i>>> = input.try_parse(|input| {
Ok(match &component.kind {
SyntaxComponentKind::Length => ParsedComponent::Length(values::length::Length::parse(input)?),
SyntaxComponentKind::Number => ParsedComponent::Number(CSSNumber::parse(input)?),
SyntaxComponentKind::Percentage => {
ParsedComponent::Percentage(values::percentage::Percentage::parse(input)?)
}
SyntaxComponentKind::LengthPercentage => {
ParsedComponent::LengthPercentage(values::length::LengthPercentage::parse(input)?)
}
SyntaxComponentKind::Color => ParsedComponent::Color(values::color::CssColor::parse(input)?),
SyntaxComponentKind::Image => ParsedComponent::Image(values::image::Image::parse(input)?),
SyntaxComponentKind::Url => ParsedComponent::Url(values::url::Url::parse(input)?),
SyntaxComponentKind::Integer => ParsedComponent::Integer(CSSInteger::parse(input)?),
SyntaxComponentKind::Angle => ParsedComponent::Angle(values::angle::Angle::parse(input)?),
SyntaxComponentKind::Time => ParsedComponent::Time(values::time::Time::parse(input)?),
SyntaxComponentKind::Resolution => {
ParsedComponent::Resolution(values::resolution::Resolution::parse(input)?)
}
SyntaxComponentKind::TransformFunction => {
ParsedComponent::TransformFunction(crate::properties::transform::Transform::parse(input)?)
}
SyntaxComponentKind::TransformList => {
ParsedComponent::TransformList(crate::properties::transform::TransformList::parse(input)?)
}
SyntaxComponentKind::CustomIdent => {
ParsedComponent::CustomIdent(values::ident::CustomIdent::parse(input)?)
}
SyntaxComponentKind::Literal(value) => {
let location = input.current_source_location();
let ident = input.expect_ident()?;
if *ident != &value {
return Err(location.new_unexpected_token_error(Token::Ident(ident.clone())));
}
ParsedComponent::Literal(ident.into())
}
})
});
if let Ok(value) = value {
match component.multiplier {
Multiplier::None => return Ok(value),
Multiplier::Space => {
parsed.push(value);
if input.is_exhausted() {
return Ok(ParsedComponent::Repeated {
components: parsed,
multiplier: component.multiplier.clone(),
});
}
}
Multiplier::Comma => {
parsed.push(value);
match input.next() {
Err(_) => {
return Ok(ParsedComponent::Repeated {
components: parsed,
multiplier: component.multiplier.clone(),
})
}
Ok(&Token::Comma) => continue,
Ok(_) => break,
}
}
}
} else {
break;
}
}
input.reset(&state);
}
Err(input.new_error_for_next_token())
}
}
}
/// Parses a value from a string according to the syntax grammar.
pub fn parse_value_from_string<'t>(
&self,
input: &'i str,
) -> Result<ParsedComponent<'i>, ParseError<'i, ParserError<'i>>> {
let mut input = ParserInput::new(input);
let mut parser = Parser::new(&mut input);
self.parse_value(&mut parser)
}
}
impl SyntaxComponent {
fn parse_string(input: &mut &str) -> Result<SyntaxComponent, ()> {
let kind = SyntaxComponentKind::parse_string(input)?;
// Pre-multiplied types cannot have multipliers.
if kind == SyntaxComponentKind::TransformList {
return Ok(SyntaxComponent {
kind,
multiplier: Multiplier::None,
});
}
let multiplier = if input.starts_with('+') {
*input = &input[1..];
Multiplier::Space
} else if input.starts_with('#') {
*input = &input[1..];
Multiplier::Comma
} else {
Multiplier::None
};
Ok(SyntaxComponent { kind, multiplier })
}
}
// https://drafts.csswg.org/css-syntax-3/#whitespace
static SPACE_CHARACTERS: &'static [char] = &['\u{0020}', '\u{0009}'];
impl SyntaxComponentKind {
fn parse_string(input: &mut &str) -> Result<SyntaxComponentKind, ()> {
// https://drafts.css-houdini.org/css-properties-values-api/#consume-syntax-component
*input = input.trim_start_matches(SPACE_CHARACTERS);
if input.starts_with('<') {
// https://drafts.css-houdini.org/css-properties-values-api/#consume-data-type-name
let end_idx = input.find('>').ok_or(())?;
let name = &input[1..end_idx];
let component = match_ignore_ascii_case! {name,
"length" => SyntaxComponentKind::Length,
"number" => SyntaxComponentKind::Number,
"percentage" => SyntaxComponentKind::Percentage,
"length-percentage" => SyntaxComponentKind::LengthPercentage,
"color" => SyntaxComponentKind::Color,
"image" => SyntaxComponentKind::Image,
"url" => SyntaxComponentKind::Url,
"integer" => SyntaxComponentKind::Integer,
"angle" => SyntaxComponentKind::Angle,
"time" => SyntaxComponentKind::Time,
"resolution" => SyntaxComponentKind::Resolution,
"transform-function" => SyntaxComponentKind::TransformFunction,
"transform-list" => SyntaxComponentKind::TransformList,
"custom-ident" => SyntaxComponentKind::CustomIdent,
_ => return Err(())
};
*input = &input[end_idx + 1..];
Ok(component)
} else if input.starts_with(is_ident_start) {
// A literal.
let end_idx = input.find(|c| !is_name_code_point(c)).unwrap_or_else(|| input.len());
let name = input[0..end_idx].to_owned();
*input = &input[end_idx..];
Ok(SyntaxComponentKind::Literal(name))
} else {
return Err(());
}
}
}
#[inline]
fn is_ident_start(c: char) -> bool {
// https://drafts.csswg.org/css-syntax-3/#ident-start-code-point
c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '\u{80}' || c == '_'
}
#[inline]
fn is_name_code_point(c: char) -> bool {
// https://drafts.csswg.org/css-syntax-3/#ident-code-point
is_ident_start(c) || c >= '0' && c <= '9' || c == '-'
}
impl<'i> Parse<'i> for SyntaxString {
fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let string = input.expect_string_cloned()?;
SyntaxString::parse_string(string.as_ref()).map_err(|_| input.new_custom_error(ParserError::InvalidValue))
}
}
impl ToCss for SyntaxString {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
dest.write_char('"')?;
match self {
SyntaxString::Universal => dest.write_char('*')?,
SyntaxString::Components(components) => {
let mut first = true;
for component in components {
if first {
first = false;
} else {
dest.delim('|', true)?;
}
component.to_css(dest)?;
}
}
}
dest.write_char('"')
}
}
impl ToCss for SyntaxComponent {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
self.kind.to_css(dest)?;
match self.multiplier {
Multiplier::None => Ok(()),
Multiplier::Comma => dest.write_char('#'),
Multiplier::Space => dest.write_char('+'),
}
}
}
impl ToCss for SyntaxComponentKind {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
use SyntaxComponentKind::*;
let s = match self {
Length => "<length>",
Number => "<number>",
Percentage => "<percentage>",
LengthPercentage => "<length-percentage>",
Color => "<color>",
Image => "<image>",
Url => "<url>",
Integer => "<integer>",
Angle => "<angle>",
Time => "<time>",
Resolution => "<resolution>",
TransformFunction => "<transform-function>",
TransformList => "<transform-list>",
CustomIdent => "<custom-ident>",
Literal(l) => l,
};
dest.write_str(s)
}
}
impl<'i> ToCss for ParsedComponent<'i> {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
use ParsedComponent::*;
match self {
Length(v) => v.to_css(dest),
Number(v) => v.to_css(dest),
Percentage(v) => v.to_css(dest),
LengthPercentage(v) => v.to_css(dest),
Color(v) => v.to_css(dest),
Image(v) => v.to_css(dest),
Url(v) => v.to_css(dest),
Integer(v) => v.to_css(dest),
Angle(v) => v.to_css(dest),
Time(v) => v.to_css(dest),
Resolution(v) => v.to_css(dest),
TransformFunction(v) => v.to_css(dest),
TransformList(v) => v.to_css(dest),
CustomIdent(v) => v.to_css(dest),
Literal(v) => v.to_css(dest),
Repeated { components, multiplier } => {
let mut first = true;
for component in components {
if first {
first = false;
} else {
match multiplier {
Multiplier::Comma => dest.delim(',', false)?,
Multiplier::Space => dest.write_char(' ')?,
Multiplier::None => unreachable!(),
}
}
component.to_css(dest)?;
}
Ok(())
}
TokenList(t) => t.to_css(dest, false),
}
}
}
#[cfg(test)]
mod tests {
use crate::values::color::RGBA;
use super::*;
fn test(source: &str, test: &str, expected: ParsedComponent) {
let parsed = SyntaxString::parse_string(source).unwrap();
let mut input = ParserInput::new(test);
let mut parser = Parser::new(&mut input);
let value = parsed.parse_value(&mut parser).unwrap();
assert_eq!(value, expected);
}
fn parse_error_test(source: &str) {
let res = SyntaxString::parse_string(source);
match res {
Ok(_) => unreachable!(),
Err(_) => {}
}
}
fn error_test(source: &str, test: &str) {
let parsed = SyntaxString::parse_string(source).unwrap();
let mut input = ParserInput::new(test);
let mut parser = Parser::new(&mut input);
let res = parsed.parse_value(&mut parser);
match res {
Ok(_) => unreachable!(),
Err(_) => {}
}
}
#[test]
fn test_syntax() {
test(
"foo | <color>+ | <integer>",
"foo",
ParsedComponent::Literal("foo".into()),
);
test("foo|<color>+|<integer>", "foo", ParsedComponent::Literal("foo".into()));
test("foo | <color>+ | <integer>", "2", ParsedComponent::Integer(2));
test(
"foo | <color>+ | <integer>",
"red",
ParsedComponent::Repeated {
components: vec![ParsedComponent::Color(values::color::CssColor::RGBA(RGBA {
red: 255,
green: 0,
blue: 0,
alpha: 255,
}))],
multiplier: Multiplier::Space,
},
);
test(
"foo | <color>+ | <integer>",
"red blue",
ParsedComponent::Repeated {
components: vec![
ParsedComponent::Color(values::color::CssColor::RGBA(RGBA {
red: 255,
green: 0,
blue: 0,
alpha: 255,
})),
ParsedComponent::Color(values::color::CssColor::RGBA(RGBA {
red: 0,
green: 0,
blue: 255,
alpha: 255,
})),
],
multiplier: Multiplier::Space,
},
);
error_test("foo | <color>+ | <integer>", "2.5");
error_test("foo | <color>+ | <integer>", "25px");
error_test("foo | <color>+ | <integer>", "red, green");
test(
"foo | <color># | <integer>",
"red, blue",
ParsedComponent::Repeated {
components: vec![
ParsedComponent::Color(values::color::CssColor::RGBA(RGBA {
red: 255,
green: 0,
blue: 0,
alpha: 255,
})),
ParsedComponent::Color(values::color::CssColor::RGBA(RGBA {
red: 0,
green: 0,
blue: 255,
alpha: 255,
})),
],
multiplier: Multiplier::Comma,
},
);
error_test("foo | <color># | <integer>", "red green");
test(
"<length>",
"25px",
ParsedComponent::Length(values::length::Length::Value(values::length::LengthValue::Px(25.0))),
);
test(
"<length>",
"calc(25px + 25px)",
ParsedComponent::Length(values::length::Length::Value(values::length::LengthValue::Px(50.0))),
);
test(
"<length> | <percentage>",
"25px",
ParsedComponent::Length(values::length::Length::Value(values::length::LengthValue::Px(25.0))),
);
test(
"<length> | <percentage>",
"25%",
ParsedComponent::Percentage(values::percentage::Percentage(0.25)),
);
error_test("<length> | <percentage>", "calc(100% - 25px)");
test("foo | bar | baz", "bar", ParsedComponent::Literal("bar".into()));
test(
"<custom-ident>",
"hi",
ParsedComponent::CustomIdent(values::ident::CustomIdent("hi".into())),
);
parse_error_test("<transform-list>#");
parse_error_test("<color");
parse_error_test("color>");
}
}