use cssparser::{Delimiter, ParseError, Parser, Token};
pub(super) struct Rule<'a> {
pub source: &'a str,
selector: &'a str,
declarations: &'a str,
tokenized: bool,
}
pub(super) struct Rules<'a> {
remaining: &'a str,
compatibility: bool,
}
impl<'a> Rules<'a> {
pub fn new(source: &'a str) -> Self {
Self {
remaining: source,
compatibility: !crate::css_syntax::fits_inline_block_stack(source),
}
}
}
impl<'a> Iterator for Rules<'a> {
type Item = Result<Rule<'a>, ()>;
fn next(&mut self) -> Option<Self::Item> {
let source = self.remaining;
if source.trim_start().is_empty() {
return None;
}
let result = if self.compatibility {
compatibility_rule(source)
} else {
tokenized_rule(source).or_else(|_| {
self.compatibility = true;
compatibility_rule(source)
})
};
self.remaining = result
.as_ref()
.map_or("", |rule| &source[rule.source.len()..]);
Some(result)
}
}
fn tokenized_rule(source: &str) -> Result<Rule<'_>, ()> {
let parser = &mut Parser::new(source);
let start = parser.position();
let selector = parser
.parse_until_before(Delimiter::CurlyBracketBlock, |p| component_slice(p, 0))
.map_err(|_| ())?;
parser.expect_curly_bracket_block().map_err(|_| ())?;
let (declarations, end) = parser
.parse_nested_block(|p| Ok((component_slice(p, 0)?, p.position())))
.map_err(|_: ParseError<()>| ())?;
if parser.slice(end..parser.position()) != "}"
|| selector.contains(['{', '}'])
|| declarations.contains(['{', '}'])
{
return Err(());
}
Ok(Rule {
source: parser.slice_from(start),
selector,
declarations,
tokenized: true,
})
}
impl Rule<'_> {
pub fn admitted(&self) -> bool {
!self.selector.trim().is_empty()
&& !self.selector.contains([':', '}'])
&& !self.declarations.contains('{')
&& self
.tokenized
.then(|| tokenized_declarations(self.declarations).ok())
.flatten()
.unwrap_or_else(|| {
self.declarations.split(';').all(compatibility_declaration)
})
}
}
fn tokenized_declarations(source: &str) -> Result<bool, ()> {
let parser = &mut Parser::new(source);
while !parser.is_exhausted() {
let admitted = parser
.parse_until_before(Delimiter::Semicolon, |declaration| {
let start = declaration.position();
if declaration.is_exhausted() {
return Ok(true);
}
declaration.expect_ident()?;
let property = declaration.slice_from(start);
declaration.expect_colon()?;
let value = component_slice(declaration, 0)?;
if declaration.slice_from(start).contains(';') || property.contains(':') {
return Err(ParseError::custom(()));
}
Ok(admitted_declaration(property, value))
})
.map_err(|_: ParseError<()>| ())?;
if !admitted {
return Ok(false);
}
if !parser.is_exhausted() {
parser.expect_semicolon().map_err(|_| ())?;
}
}
Ok(true)
}
fn admitted_declaration(property: &str, value: &str) -> bool {
let property = property.trim();
!property.is_empty()
&& property
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-')
&& super::allowed_static_style_property(property)
&& !value.trim().is_empty()
}
fn component_slice<'i>(parser: &mut Parser<'i>, depth: u32) -> Result<&'i str, ParseError<()>> {
if depth == 128 {
return Err(ParseError::custom(()));
}
let start = parser.position();
while let Ok(token) = parser.next_including_whitespace_and_comments() {
let close = match token {
Token::Function(_) | Token::ParenthesisBlock => ")",
Token::SquareBracketBlock => "]",
Token::CurlyBracketBlock => "}",
token if token.is_parse_error() => return Err(ParseError::custom(())),
_ => continue,
};
let end = parser.parse_nested_block(|p| {
component_slice(p, depth + 1)?;
Ok(p.position())
})?;
if parser.slice(end..parser.position()) != close {
return Err(ParseError::custom(()));
}
}
Ok(parser.slice_from(start))
}
fn compatibility_rule(source: &str) -> Result<Rule<'_>, ()> {
let (selector, after_selector) = source.split_once('{').ok_or(())?;
let (declarations, remaining) = after_selector.split_once('}').ok_or(())?;
Ok(Rule {
source: &source[..source.len() - remaining.len()],
selector,
declarations,
tokenized: false,
})
}
fn compatibility_declaration(source: &str) -> bool {
source.trim().is_empty()
|| source
.split_once(':')
.is_some_and(|(p, v)| admitted_declaration(p, v))
}
pub(super) fn without_at_rules(source: &str) -> Option<(std::borrow::Cow<'_, str>, bool)> {
if !source.contains('@') {
return Some((std::borrow::Cow::Borrowed(source), false));
}
if !crate::css_syntax::fits_inline_block_stack(source) {
return None;
}
let parser = &mut Parser::new(source);
let mut removed = Vec::new();
let mut boundary = true;
loop {
let start = parser.position().byte_index();
let Ok(token) = parser.next_including_whitespace_and_comments() else {
break;
};
match token {
Token::WhiteSpace(_) | Token::CDO | Token::CDC => {}
Token::AtKeyword(_) if boundary => {
loop {
match parser.next_including_whitespace_and_comments() {
Ok(Token::Semicolon) | Err(_) => break,
Ok(Token::CurlyBracketBlock) => {
parser
.parse_nested_block(|block| {
while block.next_including_whitespace_and_comments().is_ok() {}
Ok::<(), ParseError<()>>(())
})
.ok()?;
break;
}
Ok(token) if token.is_parse_error() => return None,
Ok(_) => {}
}
}
removed.push(start..parser.position().byte_index());
}
Token::CurlyBracketBlock => boundary = true,
token if token.is_parse_error() => return None,
_ => boundary = false,
}
}
if removed.is_empty() {
return Some((std::borrow::Cow::Borrowed(source), false));
}
let mut kept = String::new();
kept.try_reserve(source.len()).ok()?;
let mut copied = 0;
for range in removed {
kept.push_str(source.get(copied..range.start)?);
copied = range.end;
}
kept.push_str(source.get(copied..)?);
Some((std::borrow::Cow::Owned(kept), true))
}