use std::borrow::Cow;
use std::fmt;
use nom::branch::{alt, Alt};
use nom::bytes::complete::{is_not, tag, take_while1};
use nom::character::complete::{alpha1, alphanumeric1, anychar, char, none_of};
use nom::combinator::{fail, map, not, opt, recognize};
use nom::error::{convert_error, ErrorKind, ParseError, VerboseError};
use nom::multi::{many0, many1, separated_list1};
use nom::sequence::{delimited, pair, preceded, separated_pair, terminated};
use nom::IResult;
use crate::ast::{
Block, Rule, RuleBlockContent, ScopeContent, Selector, Sheet, StringFragment, StyleAttribute,
};
use crate::bow::Bow;
use crate::{Error, Result};
#[cfg(test)]
use log::trace;
#[derive(Debug, PartialEq)]
enum RuleBlockKind {
Keyframes,
Other,
}
#[allow(clippy::let_and_return)]
fn traced_context<I, O, F>(
ctx: &'static str,
mut p: impl nom::Parser<I, O, F>,
) -> impl FnMut(I) -> IResult<I, O, F>
where
I: fmt::Display + fmt::Debug + Clone,
O: fmt::Debug,
F: fmt::Debug + nom::error::ContextError<I>,
{
use nom::error::context;
#[cfg(test)]
use std::sync::atomic::{AtomicUsize, Ordering};
#[cfg(test)]
thread_local! {
static NESTING_LEVEL: AtomicUsize = AtomicUsize::default();
}
move |i| {
#[cfg(test)]
let nesting_lvl = NESTING_LEVEL.with(|lvl| lvl.fetch_add(1, Ordering::Relaxed));
#[cfg(test)]
trace!("> {} {}: {}", nesting_lvl, ctx, i);
let result = context(ctx, |i| p.parse(i))(i);
#[cfg(test)]
trace!("< {} {}: {:#?}", nesting_lvl, ctx, result);
#[cfg(test)]
NESTING_LEVEL.with(|lvl| lvl.fetch_sub(1, Ordering::Relaxed));
result
}
}
fn expect_non_empty<'a, O, F>(
mut p: impl nom::Parser<&'a str, O, F>,
) -> impl FnMut(&'a str) -> IResult<&'a str, O, F>
where
F: nom::error::ParseError<&'a str>,
{
move |i| {
if i.is_empty() {
Err(nom::Err::Error(ParseError::from_error_kind(
i,
ErrorKind::LengthValue,
)))
} else {
p.parse(i)
}
}
}
#[derive(Debug)]
pub(crate) struct Parser {}
#[allow(clippy::let_and_return)]
impl Parser {
fn trimmed<'a, O, F>(f: F) -> impl FnMut(&'a str) -> IResult<&'a str, O, VerboseError<&str>>
where
F: nom::Parser<&'a str, O, VerboseError<&'a str>>,
O: std::fmt::Debug,
{
traced_context(
"Trimmed",
delimited(
Self::sp,
f,
Self::sp,
),
)
}
fn cmt(i: &str) -> IResult<&str, &str, VerboseError<&str>> {
traced_context(
"Comment",
delimited(
tag("/*"),
recognize(many0(alt((
is_not("*"),
terminated(tag("*"), not(char('/'))),
)))),
tag("*/"),
),
)(i)
}
fn sp(i: &str) -> IResult<&str, &str, VerboseError<&str>> {
let chars = " \t\r\n";
let whitespace = take_while1(move |c| chars.contains(c));
traced_context("Whitespace", recognize(many0(alt((whitespace, Self::cmt)))))(i)
}
fn ident(i: &str) -> IResult<&str, &str, VerboseError<&str>> {
traced_context(
"Ident",
recognize(preceded(
alt((
tag("-"),
tag("_"),
alpha1,
take_while1(|m: char| !m.is_ascii()),
)),
many0(alt((
tag("-"),
tag("_"),
alphanumeric1,
take_while1(|m: char| !m.is_ascii()),
))),
)),
)(i)
}
fn style_attr_key(i: &str) -> IResult<&str, &str, VerboseError<&str>> {
traced_context("StyleAttrKey", Self::trimmed(Self::ident))(i)
}
fn style_attr_value(i: &str) -> IResult<&str, StringFragment, VerboseError<&str>> {
traced_context(
"StyleAttrValue",
Self::trimmed(map(
recognize(many1(alt((
is_not("${;}/\""),
recognize(Self::interpolation),
Self::string,
recognize(preceded(tag("/"), none_of("${;}\"*"))),
)))),
|m: &str| StringFragment {
inner: m.to_string().trim().to_string().into(),
},
)),
)(i)
}
fn attribute(i: &str) -> IResult<&str, StyleAttribute, VerboseError<&str>> {
traced_context(
"StyleAttribute",
Self::trimmed(expect_non_empty(map(
separated_pair(
Self::style_attr_key,
tag(":"),
Self::style_attr_value,
),
move |p: (&str, StringFragment)| StyleAttribute {
key: p.0.trim().to_string().into(),
value: vec![p.1].into(),
},
))),
)(i)
}
fn attributes(
i: &str,
dangling: bool,
) -> IResult<&str, Vec<StyleAttribute>, VerboseError<&str>> {
let final_semicolon = |i| {
if dangling {
map(tag(";"), Some)(i)
} else {
opt(tag(";"))(i)
}
};
traced_context(
"StyleAttributes",
Self::trimmed(expect_non_empty(terminated(
separated_list1(tag(";"), Self::attribute),
preceded(opt(Self::sp), final_semicolon),
))),
)(i)
}
fn string(i: &str) -> IResult<&str, &str, VerboseError<&str>> {
let escaped_char = traced_context("EscapedChar", recognize(preceded(tag("\\"), anychar)));
let parse_str = recognize(preceded(
tag("\""),
terminated(many0(alt((is_not(r#"\""#), escaped_char))), tag("\"")),
));
traced_context("String", Self::trimmed(expect_non_empty(parse_str)))(i)
}
fn interpolation(i: &str) -> IResult<&str, &str, VerboseError<&str>> {
traced_context(
"Interpolation",
Self::trimmed(expect_non_empty(delimited(
tag("${"),
Self::trimmed(recognize(preceded(
alpha1,
many0(alt((alphanumeric1, tag("_")))),
))),
tag("}"),
))),
)(i)
}
fn selector(i: &str) -> IResult<&str, Selector, VerboseError<&str>> {
traced_context(
"Selector",
Self::trimmed(expect_non_empty(map(
recognize(many1(alt((
recognize(preceded(none_of("$,}@{\""), opt(is_not("$,\"{")))),
Self::string,
recognize(Self::interpolation),
)))),
|p: &str| vec![p.trim().to_owned().into()].into(),
))),
)(i)
}
fn condition(i: &str) -> IResult<&str, Vec<Selector>, VerboseError<&str>> {
traced_context(
"Condition",
Self::trimmed(expect_non_empty(many1(terminated(
Self::selector,
opt(tag(",")),
)))),
)(i)
}
fn block_contents(i: &str) -> IResult<&str, Vec<RuleBlockContent>, VerboseError<&str>> {
traced_context(
"BlockContents",
Self::trimmed(expect_non_empty(map(
many0(alt((
map(
|i| Parser::attributes(i, false),
|m| m.into_iter().map(RuleBlockContent::StyleAttr).collect(),
),
map(
|i| Parser::rule_block(i, RuleBlockKind::Other),
|m| vec![RuleBlockContent::Rule(Bow::Boxed(Box::new(m)))],
),
))),
|m: Vec<Vec<RuleBlockContent>>| m.into_iter().flatten().collect(),
))),
)(i)
}
fn block(i: &str) -> IResult<&str, ScopeContent, VerboseError<&str>> {
traced_context(
"Block",
Self::trimmed(expect_non_empty(map(
pair(
Self::condition,
delimited(tag("{"), Self::trimmed(Self::block_contents), tag("}")),
),
|p: (Vec<Selector>, Vec<RuleBlockContent>)| {
ScopeContent::Block(Block {
condition: p.0.into(),
content: p.1.into(),
})
},
))),
)(i)
}
fn rule_block_contents(i: &str) -> IResult<&str, Vec<RuleBlockContent>, VerboseError<&str>> {
map(
traced_context(
"RuleBlockContents",
expect_non_empty(many0(alt((
map(
|i| Parser::attributes(i, false),
|m: Vec<StyleAttribute>| {
m.into_iter().map(RuleBlockContent::StyleAttr).collect()
},
),
map(
|i| Parser::rule_block(i, RuleBlockKind::Other),
|m: Rule| vec![RuleBlockContent::Rule(Bow::Boxed(Box::new(m)))],
),
)))),
),
|m: Vec<Vec<RuleBlockContent>>| m.into_iter().flatten().collect(),
)(i)
}
fn rule_block(i: &str, kind: RuleBlockKind) -> IResult<&str, Rule, VerboseError<&str>> {
let cond = |i| match kind {
RuleBlockKind::Other => {
Self::at_rule_condition(i, (tag("@container"), tag("@media"), tag("@supports")))
}
RuleBlockKind::Keyframes => map(recognize(Self::condition), |m| {
vec![m.trim().to_string().into()]
})(i),
};
traced_context(
"RuleBlock",
Self::trimmed(expect_non_empty(map(
separated_pair(
cond,
tag("{"),
terminated(Self::rule_block_contents, tag("}")),
),
|p: (Vec<StringFragment>, Vec<RuleBlockContent>)| Rule {
condition: p.0.into(),
content: p.1.into(),
},
))),
)(i)
}
fn dangling_block(i: &str) -> IResult<&str, ScopeContent, VerboseError<&str>> {
traced_context(
"DanglingBlock",
Self::trimmed(expect_non_empty(map(
|i| Self::attributes(i, true),
|attr: Vec<StyleAttribute>| {
ScopeContent::Block(Block {
condition: Cow::Borrowed(&[]),
content: attr
.into_iter()
.map(|m| m.into())
.collect::<Vec<RuleBlockContent>>()
.into(),
})
},
))),
)(i)
}
fn scope(i: &str) -> IResult<&str, Vec<ScopeContent>, VerboseError<&str>> {
traced_context(
"Scope",
Self::trimmed(expect_non_empty(Parser::scope_contents)),
)(i)
}
fn at_rule_condition<'a, T>(
i: &'a str,
tags: T,
) -> IResult<&'a str, Vec<StringFragment>, VerboseError<&'a str>>
where
T: Alt<&'a str, &'a str, VerboseError<&'a str>>,
{
let tags = recognize(terminated(alt(tags), tag(" ")));
traced_context(
"AtRuleCondition",
Self::trimmed(expect_non_empty(map(
pair(
tags,
map(
recognize(many1(alt((is_not("${"), recognize(Self::interpolation))))),
|m: &str| StringFragment {
inner: m.trim().to_string().into(),
},
),
),
|p: (&str, StringFragment)| {
vec![
StringFragment {
inner: p.0.to_string().into(),
},
p.1,
]
},
))),
)(i)
}
fn keyframes(i: &str) -> IResult<&str, Rule, VerboseError<&str>> {
traced_context(
"Keyframes",
Self::trimmed(map(
separated_pair(
|i| Self::at_rule_condition(i, (tag("@keyframes"), fail)),
tag("{"),
terminated(
many0(|i| Parser::rule_block(i, RuleBlockKind::Keyframes)),
tag("}"),
),
),
|p: (Vec<StringFragment>, Vec<Rule>)| Rule {
condition: p.0.into(),
content: p
.1
.into_iter()
.map(|m| RuleBlockContent::Rule(Bow::Boxed(Box::new(m))))
.collect(),
},
)),
)(i)
}
fn at_rule(i: &str) -> IResult<&str, ScopeContent, VerboseError<&str>> {
traced_context(
"AtRule",
Self::trimmed(expect_non_empty(map(
separated_pair(
|i| {
Self::at_rule_condition(
i,
(tag("@container"), tag("@supports"), tag("@media")),
)
},
tag("{"),
terminated(Parser::scope_contents, tag("}")),
),
|p: (Vec<StringFragment>, Vec<ScopeContent>)| {
ScopeContent::Rule(Rule {
condition: p.0.into(),
content: p
.1
.into_iter()
.map(|m| match m {
ScopeContent::Block(m) => {
RuleBlockContent::Block(Bow::Boxed(Box::new(m)))
}
ScopeContent::Rule(m) => {
RuleBlockContent::Rule(Bow::Boxed(Box::new(m)))
}
})
.collect(),
})
},
))),
)(i)
}
fn scope_contents(i: &str) -> IResult<&str, Vec<ScopeContent>, VerboseError<&str>> {
traced_context(
"ScopeContents",
Self::trimmed(expect_non_empty(many0(alt((
Parser::dangling_block,
Parser::block,
Parser::at_rule,
map(Parser::keyframes, ScopeContent::Rule),
))))),
)(i)
}
fn sheet(i: &str) -> IResult<&str, Sheet, VerboseError<&str>> {
traced_context(
"StyleSheet",
Self::trimmed(map(many0(Self::scope), |p: Vec<Vec<ScopeContent>>| {
Sheet::from(p.into_iter().flatten().collect::<Vec<ScopeContent>>())
})),
)(i)
}
pub fn parse(css: &str) -> Result<Sheet> {
match Self::sheet(css) {
Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => Err(Error::Parse {
reason: convert_error(css, e.clone()),
source: Some(VerboseError {
errors: e
.errors
.into_iter()
.map(|(i, e)| (i.to_string(), e))
.collect(),
}),
}),
Err(nom::Err::Incomplete(e)) => Err(Error::Parse {
reason: format!("{e:#?}"),
source: None,
}),
Ok((_, res)) => Ok(res),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn init() {
let _ = env_logger::builder().is_test(true).try_init();
}
#[test]
fn test_whitespace() {
init();
let test_str = " \n\n\t\t\t/* comment */rest";
assert_eq!(
Parser::sp(test_str).expect("Failed to parse whitespace"),
("rest", " \n\n\t\t\t/* comment */")
)
}
#[test]
fn test_empty_rule() {
init();
let test_str = r#""#;
assert!(Parser::parse(test_str)
.expect("Failed to Parse Style")
.is_empty());
}
#[test]
fn test_simple_example() {
init();
let test_str = r#"
background-color: red;
.nested {
background-color: blue;
width: 100px;
}"#;
let parsed = Parser::parse(test_str).expect("Failed to Parse Style");
let expected = Sheet::from(vec![
ScopeContent::Block(Block {
condition: Cow::Borrowed(&[]),
content: vec![StyleAttribute {
key: "background-color".into(),
value: vec!["red".into()].into(),
}
.into()]
.into(),
}),
ScopeContent::Block(Block {
condition: vec![vec![".nested".into()].into()].into(),
content: vec![
StyleAttribute {
key: "background-color".into(),
value: vec!["blue".into()].into(),
}
.into(),
StyleAttribute {
key: "width".into(),
value: vec!["100px".into()].into(),
}
.into(),
]
.into(),
}),
]);
assert_eq!(parsed, expected);
}
#[test]
fn test_simple_selector_with_at() {
init();
let test_str = r#"
background-color: red;
content: ";";
[placeholder="someone@example.com"] {
background-color: blue;
width: 100px;
}"#;
let parsed = Parser::parse(test_str).expect("Failed to Parse Style");
let expected = Sheet::from(vec![
ScopeContent::Block(Block {
condition: Cow::Borrowed(&[]),
content: vec![
StyleAttribute {
key: "background-color".into(),
value: vec!["red".into()].into(),
}
.into(),
StyleAttribute {
key: "content".into(),
value: vec![r#"";""#.into()].into(),
}
.into(),
]
.into(),
}),
ScopeContent::Block(Block {
condition: vec![vec![r#"[placeholder="someone@example.com"]"#.into()].into()]
.into(),
content: vec![
StyleAttribute {
key: "background-color".into(),
value: vec!["blue".into()].into(),
}
.into(),
StyleAttribute {
key: "width".into(),
value: vec!["100px".into()].into(),
}
.into(),
]
.into(),
}),
]);
assert_eq!(parsed, expected);
}
#[test]
fn test_simple_escape() {
init();
let test_str = r#"
[placeholder="\" {}"] {
background-color: blue;
width: 100px;
}"#;
let parsed = Parser::parse(test_str).expect("Failed to Parse Style");
let expected = Sheet::from(vec![ScopeContent::Block(Block {
condition: vec![vec![r#"[placeholder="\" {}"]"#.into()].into()].into(),
content: vec![
StyleAttribute {
key: "background-color".into(),
value: vec!["blue".into()].into(),
}
.into(),
StyleAttribute {
key: "width".into(),
value: vec!["100px".into()].into(),
}
.into(),
]
.into(),
})]);
assert_eq!(parsed, expected);
}
#[test]
fn test_rule_with_ampersand() {
init();
let test_str = r#"
&:hover {
background-color: #d0d0d9;
}"#;
let parsed = Parser::parse(test_str).expect("Failed to Parse Style");
let expected = Sheet::from(vec![ScopeContent::Block(Block {
condition: vec![vec!["&:hover".into()].into()].into(),
content: vec![StyleAttribute {
key: "background-color".into(),
value: vec!["#d0d0d9".into()].into(),
}
.into()]
.into(),
})]);
assert_eq!(parsed, expected);
}
#[test]
fn test_multiple_media_queries() -> Result<()> {
init();
let test_str = r#"
@media screen and (max-width: 500px) {
background-color: red;
}
@media screen and (max-width: 200px) {
color: yellow;
}
"#;
let parsed = Parser::parse(test_str)?;
let expected = Sheet::from(vec![
ScopeContent::Rule(Rule {
condition: vec!["@media ".into(), "screen and (max-width: 500px)".into()].into(),
content: vec![RuleBlockContent::Block(
Block {
condition: Cow::Borrowed(&[]),
content: vec![StyleAttribute {
key: "background-color".into(),
value: vec!["red".into()].into(),
}
.into()]
.into(),
}
.into(),
)]
.into(),
}),
ScopeContent::Rule(Rule {
condition: vec!["@media ".into(), "screen and (max-width: 200px)".into()].into(),
content: vec![RuleBlockContent::Block(
Block {
condition: Cow::Borrowed(&[]),
content: vec![StyleAttribute {
key: "color".into(),
value: vec!["yellow".into()].into(),
}
.into()]
.into(),
}
.into(),
)]
.into(),
}),
]);
assert_eq!(parsed, expected);
Ok(())
}
#[test]
fn test_media_query_then_normal_class() -> Result<()> {
init();
let test_str = r#"
@media screen and (max-width: 500px) {
background-color: red;
}
.some-class2 {
color: yellow;
}
"#;
let parsed = Parser::parse(test_str)?;
let expected = Sheet::from(vec![
ScopeContent::Rule(Rule {
condition: vec!["@media ".into(), "screen and (max-width: 500px)".into()].into(),
content: vec![RuleBlockContent::Block(
Block {
condition: Cow::Borrowed(&[]),
content: vec![StyleAttribute {
key: "background-color".into(),
value: vec!["red".into()].into(),
}
.into()]
.into(),
}
.into(),
)]
.into(),
}),
ScopeContent::Block(Block {
condition: vec![vec![".some-class2".into()].into()].into(),
content: vec![StyleAttribute {
key: "color".into(),
value: vec!["yellow".into()].into(),
}
.into()]
.into(),
}),
]);
assert_eq!(parsed, expected);
Ok(())
}
#[test]
fn test_selector_list() -> Result<()> {
init();
let test_str = r#"
div, span {
color: yellow;
}
&, & input {
color: pink;
}
"#;
let parsed = Parser::parse(test_str)?;
let expected = Sheet::from(vec![
ScopeContent::Block(Block {
condition: vec![vec!["div".into()].into(), vec!["span".into()].into()].into(),
content: vec![StyleAttribute {
key: "color".into(),
value: vec!["yellow".into()].into(),
}
.into()]
.into(),
}),
ScopeContent::Block(Block {
condition: vec![vec!["&".into()].into(), vec!["& input".into()].into()].into(),
content: vec![StyleAttribute {
key: "color".into(),
value: vec!["pink".into()].into(),
}
.into()]
.into(),
}),
]);
assert_eq!(parsed, expected);
Ok(())
}
#[test]
fn test_container_rule() -> Result<()> {
init();
let test_str = r#"
@container (min-width: 200px) {
background: #000;
}
"#;
let parsed = Parser::parse(test_str)?;
log::debug!("{:?}", parsed);
let expected = Sheet::from(vec![ScopeContent::Rule(Rule {
condition: vec!["@container ".into(), "(min-width: 200px)".into()].into(),
content: vec![RuleBlockContent::Block(
Block {
condition: Cow::Borrowed(&[]),
content: vec![StyleAttribute {
key: "background".into(),
value: vec!["#000".into()].into(),
}
.into()]
.into(),
}
.into(),
)]
.into(),
})]);
assert_eq!(parsed, expected);
Ok(())
}
#[test]
fn test_supports_rule() -> Result<()> {
init();
let test_str = r#"
@supports (backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px)) {
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
background-color: rgb(0, 0, 0, 0.7);
}
@supports not ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
background-color: rgb(25, 25, 25);
}
"#;
let parsed = Parser::parse(test_str)?;
let expected = Sheet::from(vec![
ScopeContent::Rule(Rule {
condition: vec![
"@supports ".into(),
"(backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))".into(),
]
.into(),
content: vec![RuleBlockContent::Block(
Block {
condition: Cow::Borrowed(&[]),
content: vec![
StyleAttribute {
key: "backdrop-filter".into(),
value: vec!["blur(2px)".into()].into(),
}
.into(),
StyleAttribute {
key: "-webkit-backdrop-filter".into(),
value: vec!["blur(2px)".into()].into(),
}
.into(),
StyleAttribute {
key: "background-color".into(),
value: vec!["rgb(0, 0, 0, 0.7)".into()].into(),
}
.into(),
]
.into(),
}
.into(),
)]
.into(),
}),
ScopeContent::Rule(Rule {
condition: vec![
"@supports ".into(),
"not ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px)))"
.into(),
]
.into(),
content: vec![RuleBlockContent::Block(
Block {
condition: Cow::Borrowed(&[]),
content: vec![StyleAttribute {
key: "background-color".into(),
value: vec!["rgb(25, 25, 25)".into()].into(),
}
.into()]
.into(),
}
.into(),
)]
.into(),
}),
]);
assert_eq!(parsed, expected);
Ok(())
}
#[test]
fn test_selectors_list_2() {
init();
assert_eq!(
Parser::selector("&").map(|m| m.1),
Ok(vec!["&".into()].into())
);
assert_eq!(
Parser::selector("& input").map(|m| m.1),
Ok(vec!["& input".into()].into())
);
}
#[test]
fn test_interpolation() {
init();
let test_str = r#"
background-color: red;
.nested, ${var_a} {
background-color: blue;
width: 100px;
}"#;
let parsed = Parser::parse(test_str).expect("Failed to Parse Style");
let expected = Sheet::from(vec![
ScopeContent::Block(Block {
condition: Cow::Borrowed(&[]),
content: vec![StyleAttribute {
key: "background-color".into(),
value: vec!["red".into()].into(),
}
.into()]
.into(),
}),
ScopeContent::Block(Block {
condition: vec![
vec![".nested".into()].into(),
vec!["${var_a}".into()].into(),
]
.into(),
content: vec![
StyleAttribute {
key: "background-color".into(),
value: vec!["blue".into()].into(),
}
.into(),
StyleAttribute {
key: "width".into(),
value: vec!["100px".into()].into(),
}
.into(),
]
.into(),
}),
]);
assert_eq!(parsed, expected);
}
#[test]
fn test_empty_block() {
init();
let test_str = r#".nested {}"#;
let parsed = Parser::parse(test_str).expect("Failed to Parse Style");
let expected = Sheet::from(vec![ScopeContent::Block(Block {
condition: vec![vec![".nested".into()].into()].into(),
content: vec![].into(),
})]);
assert_eq!(parsed, expected);
}
#[test]
fn test_dense_style() {
init();
let test_str = r#"@media print{color:black;.nested{cursor:none;}}"#;
let parsed = Parser::parse(test_str).expect("Failed to Parse Style");
let expected = Sheet::from(vec![ScopeContent::Rule(Rule {
condition: vec!["@media ".into(), "print".into()].into(),
content: vec![
RuleBlockContent::Block(
Block {
condition: vec![].into(),
content: vec![StyleAttribute {
key: "color".into(),
value: vec!["black".into()].into(),
}
.into()]
.into(),
}
.into(),
),
RuleBlockContent::Block(
Block {
condition: vec![vec![".nested".into()].into()].into(),
content: vec![StyleAttribute {
key: "cursor".into(),
value: vec!["none".into()].into(),
}
.into()]
.into(),
}
.into(),
),
]
.into(),
})]);
assert_eq!(parsed, expected);
}
#[test]
fn test_issue_36() {
init();
let test_str = r#"
position: fixed; /* Stay in place */
z-index: 1; /* Sit on top */
width: 100%; /* Full width */
height: 100%; /* Full height */
"#;
let parsed = Parser::parse(test_str).expect("Failed to Parse Style");
let expected = Sheet::from(vec![ScopeContent::Block(Block {
condition: vec![].into(),
content: vec![
StyleAttribute {
key: "position".into(),
value: vec!["fixed".into()].into(),
}
.into(),
StyleAttribute {
key: "z-index".into(),
value: vec!["1".into()].into(),
}
.into(),
StyleAttribute {
key: "width".into(),
value: vec!["100%".into()].into(),
}
.into(),
StyleAttribute {
key: "height".into(),
value: vec!["100%".into()].into(),
}
.into(),
]
.into(),
})]);
assert_eq!(parsed, expected);
}
#[test]
fn test_empty_media_rule() {
init();
let test_str = r#"@media screen and (max-width: 500px) {}"#;
let parsed = Parser::parse(test_str).expect("Failed to Parse Style");
let expected = Sheet::from(vec![ScopeContent::Rule(Rule {
condition: vec!["@media ".into(), "screen and (max-width: 500px)".into()].into(),
content: vec![].into(),
})]);
assert_eq!(parsed, expected);
}
#[test]
fn test_empty() {
init();
let test_str = r#""#;
let parsed = Parser::parse(test_str).expect("Failed to Parse Style");
let expected = Sheet::from(vec![]);
assert_eq!(parsed, expected);
}
#[test]
fn test_pseudo_sel() {
init();
let test_str = r#"
color: ${color};
span, ${sel_div} {
background-color: blue;
}
:not(${sel_root}) {
background-color: black;
}
@media screen and ${breakpoint} {
display: flex;
}
"#;
let parsed = Parser::parse(test_str).expect("Failed to Parse Style");
let expected = Sheet::from(vec![
ScopeContent::Block(Block {
condition: Cow::Borrowed(&[]),
content: vec![StyleAttribute {
key: "color".into(),
value: vec!["${color}".into()].into(),
}
.into()]
.into(),
}),
ScopeContent::Block(Block {
condition: vec![vec!["span".into()].into(), vec!["${sel_div}".into()].into()]
.into(),
content: vec![StyleAttribute {
key: "background-color".into(),
value: vec!["blue".into()].into(),
}
.into()]
.into(),
}),
ScopeContent::Block(Block {
condition: vec![vec![":not(${sel_root})".into()].into()].into(),
content: vec![StyleAttribute {
key: "background-color".into(),
value: vec!["black".into()].into(),
}
.into()]
.into(),
}),
ScopeContent::Rule(Rule {
condition: vec!["@media ".into(), "screen and ${breakpoint}".into()].into(),
content: vec![RuleBlockContent::Block(
Block {
condition: Cow::Borrowed(&[]),
content: vec![StyleAttribute {
key: "display".into(),
value: vec!["flex".into()].into(),
}
.into()]
.into(),
}
.into(),
)]
.into(),
}),
]);
assert_eq!(parsed, expected);
}
#[test]
fn test_comment() {
init();
let test_str = r#"
/* some comment */ /* another comment */
color: /**/${color};
span, ${sel_div} {
/* comment before block */
background-color /* comment before colon */ : blue /* comment after attribute */;
/* comment after block */
}
:not(${sel_root}) {
background-color: black;
}
@media screen and ${breakpoint} {
/* a comment with * */
display: flex;
}
/* end comment */
"#;
let parsed = Parser::parse(test_str).expect("Failed to Parse Style");
let expected = Sheet::from(vec![
ScopeContent::Block(Block {
condition: Cow::Borrowed(&[]),
content: vec![StyleAttribute {
key: "color".into(),
value: vec!["${color}".into()].into(),
}
.into()]
.into(),
}),
ScopeContent::Block(Block {
condition: vec![vec!["span".into()].into(), vec!["${sel_div}".into()].into()]
.into(),
content: vec![StyleAttribute {
key: "background-color".into(),
value: vec!["blue".into()].into(),
}
.into()]
.into(),
}),
ScopeContent::Block(Block {
condition: vec![vec![":not(${sel_root})".into()].into()].into(),
content: vec![StyleAttribute {
key: "background-color".into(),
value: vec!["black".into()].into(),
}
.into()]
.into(),
}),
ScopeContent::Rule(Rule {
condition: vec!["@media ".into(), "screen and ${breakpoint}".into()].into(),
content: vec![RuleBlockContent::Block(
Block {
condition: Cow::Borrowed(&[]),
content: vec![StyleAttribute {
key: "display".into(),
value: vec!["flex".into()].into(),
}
.into()]
.into(),
}
.into(),
)]
.into(),
}),
]);
assert_eq!(parsed, expected);
}
#[test]
fn test_pseudo_sel_escaped() {
init();
let test_str = r#"
color: "$${color}";
span, ${sel_div} {
background-color: blue;
}
:not(${sel_root}) {
background-color: black;
}
@media screen and ${breakpoint} {
display: flex;
}
"#;
let parsed = Parser::parse(test_str).expect("Failed to Parse Style");
let expected = Sheet::from(vec![
ScopeContent::Block(Block {
condition: Cow::Borrowed(&[]),
content: vec![StyleAttribute {
key: "color".into(),
value: vec!["\"$${color}\"".into()].into(),
}
.into()]
.into(),
}),
ScopeContent::Block(Block {
condition: vec![vec!["span".into()].into(), vec!["${sel_div}".into()].into()]
.into(),
content: vec![StyleAttribute {
key: "background-color".into(),
value: vec!["blue".into()].into(),
}
.into()]
.into(),
}),
ScopeContent::Block(Block {
condition: vec![vec![":not(${sel_root})".into()].into()].into(),
content: vec![StyleAttribute {
key: "background-color".into(),
value: vec!["black".into()].into(),
}
.into()]
.into(),
}),
ScopeContent::Rule(Rule {
condition: vec!["@media ".into(), "screen and ${breakpoint}".into()].into(),
content: vec![RuleBlockContent::Block(
Block {
condition: Cow::Borrowed(&[]),
content: vec![StyleAttribute {
key: "display".into(),
value: vec!["flex".into()].into(),
}
.into()]
.into(),
}
.into(),
)]
.into(),
}),
]);
assert_eq!(parsed, expected);
}
#[test]
fn test_rule_block() {
let test_str = r#"
span {
@media screen and (max-width: 500px) {
background-color: blue;
}
}
div {
@supports (max-width: 500px) {
@media screen and (max-width: 500px) {
background-color: blue;
}
}
}
@media screen and ${breakpoint} {
display: flex;
}
"#;
let parsed = Parser::parse(test_str).expect("Failed to Parse Style");
let expected = Sheet::from(vec![
ScopeContent::Block(Block {
condition: vec![vec!["span".into()].into()].into(),
content: vec![RuleBlockContent::Rule(
Rule {
condition: vec!["@media ".into(), "screen and (max-width: 500px)".into()]
.into(),
content: vec![RuleBlockContent::StyleAttr(StyleAttribute {
key: "background-color".into(),
value: vec!["blue".into()].into(),
})]
.into(),
}
.into(),
)]
.into(),
}),
ScopeContent::Block(Block {
condition: vec![vec!["div".into()].into()].into(),
content: vec![RuleBlockContent::Rule(
Rule {
condition: vec!["@supports ".into(), "(max-width: 500px)".into()].into(),
content: vec![RuleBlockContent::Rule(
Rule {
condition: vec![
"@media ".into(),
"screen and (max-width: 500px)".into(),
]
.into(),
content: vec![RuleBlockContent::StyleAttr(StyleAttribute {
key: "background-color".into(),
value: vec!["blue".into()].into(),
})]
.into(),
}
.into(),
)]
.into(),
}
.into(),
)]
.into(),
}),
ScopeContent::Rule(Rule {
condition: vec!["@media ".into(), "screen and ${breakpoint}".into()].into(),
content: vec![RuleBlockContent::Block(
Block {
condition: Cow::Borrowed(&[]),
content: vec![StyleAttribute {
key: "display".into(),
value: vec!["flex".into()].into(),
}
.into()]
.into(),
}
.into(),
)]
.into(),
}),
]);
assert_eq!(parsed, expected);
}
#[test]
fn test_slash_parsing() {
let test_str = r#"
grid-row: 1 / 3;
grid-row: 1/3;
grid-row: 1/ 3;
grid-row: 1 /3;
"#;
let parsed = Parser::parse(test_str).expect("Failed to Parse Style");
let expected = Sheet::from(vec![ScopeContent::Block(Block {
condition: vec![].into(),
content: vec![
RuleBlockContent::StyleAttr(StyleAttribute {
key: "grid-row".into(),
value: vec!["1 / 3".into()].into(),
}),
RuleBlockContent::StyleAttr(StyleAttribute {
key: "grid-row".into(),
value: vec!["1/3".into()].into(),
}),
RuleBlockContent::StyleAttr(StyleAttribute {
key: "grid-row".into(),
value: vec!["1/ 3".into()].into(),
}),
RuleBlockContent::StyleAttr(StyleAttribute {
key: "grid-row".into(),
value: vec!["1 /3".into()].into(),
}),
]
.into(),
})]);
assert_eq!(parsed, expected);
}
#[test]
fn test_url() {
let test_str = r#"
background-image: url(https://example.com/example.jpg);
"#;
let parsed = Parser::parse(test_str).expect("Failed to Parse Style");
let expected = Sheet::from(vec![ScopeContent::Block(Block {
condition: vec![].into(),
content: vec![RuleBlockContent::StyleAttr(StyleAttribute {
key: "background-image".into(),
value: vec!["url(https://example.com/example.jpg)".into()].into(),
})]
.into(),
})]);
assert_eq!(parsed, expected);
}
}