use super::ident::Ident;
use super::number::{CSSInteger, CSSNumber};
use crate::error::{ParserError, PrinterError};
use crate::printer::Printer;
use crate::traits::{Parse, ToCss};
use crate::values;
#[cfg(feature = "visitor")]
use crate::visitor::Visit;
use cssparser::*;
#[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))]
pub enum SyntaxString {
Components(Vec<SyntaxComponent>),
Universal,
}
#[derive(Debug, PartialEq, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct SyntaxComponent {
pub kind: SyntaxComponentKind,
pub multiplier: Multiplier,
}
#[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))]
pub enum SyntaxComponentKind {
Length,
Number,
Percentage,
LengthPercentage,
Color,
Image,
Url,
Integer,
Angle,
Time,
Resolution,
TransformFunction,
TransformList,
CustomIdent,
Literal(String), }
#[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))]
pub enum Multiplier {
None,
Space,
Comma,
}
#[derive(Debug, PartialEq, Clone)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "into_owned", derive(lightningcss_derive::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> {
Length(values::length::Length),
Number(CSSNumber),
Percentage(values::percentage::Percentage),
LengthPercentage(values::length::LengthPercentage),
Color(values::color::CssColor),
#[cfg_attr(feature = "serde", serde(borrow))]
Image(values::image::Image<'i>),
Url(values::url::Url<'i>),
Integer(CSSInteger),
Angle(values::angle::Angle),
Time(values::time::Time),
Resolution(values::resolution::Resolution),
TransformFunction(crate::properties::transform::Transform),
TransformList(crate::properties::transform::TransformList),
CustomIdent(values::ident::CustomIdent<'i>),
Literal(Ident<'i>),
Repeated {
#[cfg_attr(feature = "visitor", skip_type)]
components: Vec<ParsedComponent<'i>>,
multiplier: Multiplier,
},
Token(crate::properties::custom::Token<'i>),
}
impl<'i> SyntaxString {
pub fn parse_string(input: &'i str) -> Result<SyntaxString, ()> {
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))
}
pub fn parse_value<'t>(
&self,
input: &mut Parser<'i, 't>,
) -> Result<ParsedComponent<'i>, ParseError<'i, ParserError<'i>>> {
match self {
SyntaxString::Universal => Ok(ParsedComponent::Token(crate::properties::custom::Token::from(
input.next()?,
))),
SyntaxString::Components(components) => {
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())
}
}
}
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)?;
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 })
}
}
static SPACE_CHARACTERS: &'static [char] = &['\u{0020}', '\u{0009}'];
impl SyntaxComponentKind {
fn parse_string(input: &mut &str) -> Result<SyntaxComponentKind, ()> {
*input = input.trim_start_matches(SPACE_CHARACTERS);
if input.starts_with('<') {
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) {
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 {
c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '\u{80}' || c == '_'
}
#[inline]
fn is_name_code_point(c: char) -> bool {
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(())
}
Token(t) => t.to_css(dest),
}
}
}
#[cfg(test)]
mod tests {
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>");
}
}