use cssparser::{
AtRuleParser, BasicParseErrorKind, CowRcStr, Delimiter, ParseError, Parser as CssParser,
ParserInput, ParserState, QualifiedRuleParser, StyleSheetParser, parse_important,
};
use indexmap::IndexMap;
use log::{debug, warn};
use std::str::FromStr;
use std::sync::{Arc, OnceLock};
use super::cascade::CascadeIndex;
use super::{
properties::CssAttr,
selector::CssSelector,
values::{
alignment_longhand_valid, expand_place_shorthand, is_alignment_longhand,
is_place_shorthand_name,
},
};
#[derive(Debug, Clone)]
pub struct CssRuleEntry {
pub selector: CssSelector,
pub normal: CssAttr,
pub important: CssAttr,
}
#[derive(Debug, Clone)]
pub struct CssRule(
pub Vec<CssRuleEntry>,
pub(crate) OnceLock<CascadeIndex>,
);
impl CssRule {
pub fn new() -> Self {
CssRule(Vec::new(), OnceLock::new())
}
pub(crate) fn cascade_index(&self) -> &CascadeIndex {
self.1.get_or_init(|| CascadeIndex::build(self))
}
pub fn push(&mut self, selector: CssSelector, normal: CssAttr, important: CssAttr) {
self.1.take();
self.0.push(CssRuleEntry {
selector,
normal,
important,
});
}
pub fn add(&mut self, selector: CssSelector, attr: CssAttr) {
self.1.take();
self.0.push(CssRuleEntry {
selector,
normal: attr,
important: CssAttr::new(),
});
}
}
impl Default for CssRule {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct Parser {
context: String,
}
impl Parser {
pub fn new(input: &str) -> Self {
Parser {
context: input.to_string(),
}
}
pub fn parse(&mut self) -> CssRule {
let mut parser_input = ParserInput::new(&self.context);
let mut parser = CssParser::new(&mut parser_input);
let mut state = CssParseState::new();
{
let rule_list = StyleSheetParser::new(&mut parser, &mut state);
for rule_result in rule_list {
if let std::result::Result::Err(e) = rule_result {
warn!("CSS error: {:?}", e);
}
}
}
let mut css = CssRule::new();
for (sel_text, normal, important) in state.rules.drain(..) {
for sub_sel in split_top_level_commas(&sel_text) {
match CssSelector::from_str(sub_sel) {
std::result::Result::Ok(sel) => {
css.push(sel, normal.clone(), important.clone())
}
std::result::Result::Err(e) => {
warn!("Selector parse fail: {}: {}", sub_sel, e);
}
}
}
}
debug!("CSS parsed: {} rules", css.0.len());
css
}
}
struct CssParseState {
rules: Vec<(String, CssAttr, CssAttr)>,
}
impl CssParseState {
fn new() -> Self {
Self { rules: vec![] }
}
}
impl<'i> QualifiedRuleParser<'i> for CssParseState {
type Prelude = String;
type QualifiedRule = ();
type Error = cssparser::BasicParseError<'i>;
fn parse_prelude<'t>(
&mut self,
input: &mut CssParser<'i, 't>,
) -> std::result::Result<String, ParseError<'i, Self::Error>> {
let start = input.position();
while !input.is_exhausted() {
if input.next_including_whitespace_and_comments().is_err() {
break;
}
}
let text = input.slice_from(start).trim().to_string();
if text.is_empty() {
return Err(input.new_error(BasicParseErrorKind::QualifiedRuleInvalid));
}
Ok(text)
}
fn parse_block<'t>(
&mut self,
prelude: String,
_start: &ParserState,
input: &mut CssParser<'i, 't>,
) -> std::result::Result<(), ParseError<'i, Self::Error>> {
let mut normal = CssAttr::new();
let mut important = CssAttr::new();
parse_declarations_into(input, &mut normal, &mut important, true);
self.rules.push((prelude, normal, important));
Ok(())
}
}
impl<'i> AtRuleParser<'i> for CssParseState {
type Prelude = ();
type AtRule = ();
type Error = cssparser::BasicParseError<'i>;
fn parse_prelude<'t>(
&mut self,
name: CowRcStr<'i>,
input: &mut CssParser<'i, 't>,
) -> std::result::Result<(), ParseError<'i, Self::Error>> {
debug!("skip @{}", name);
while !input.is_exhausted() {
input.next().ok();
}
Ok(())
}
fn parse_block<'t>(
&mut self,
_: (),
_: &ParserState,
input: &mut CssParser<'i, 't>,
) -> std::result::Result<(), ParseError<'i, Self::Error>> {
while !input.is_exhausted() {
input.next().ok();
}
Ok(())
}
fn rule_without_block(&mut self, _: (), _: &ParserState) -> std::result::Result<(), ()> {
Ok(())
}
}
fn clean_value_text(raw: &str) -> Arc<str> {
let trimmed = raw.trim();
if let Some(idx) = trimmed.find("!important") {
Arc::from(trimmed[..idx].trim_end())
} else {
Arc::from(trimmed)
}
}
fn split_top_level_commas(s: &str) -> Vec<&str> {
let mut out = Vec::new();
let mut depth = 0usize;
let mut last = 0usize;
for (i, ch) in s.char_indices() {
match ch {
'(' | '[' => depth += 1,
')' | ']' => depth = depth.saturating_sub(1),
',' if depth == 0 => {
let part = s[last..i].trim();
if !part.is_empty() {
out.push(part);
}
last = i + 1;
}
_ => {}
}
}
if let Some(tail) = s.get(last..) {
let tail = tail.trim();
if !tail.is_empty() {
out.push(tail);
}
}
out
}
#[doc(hidden)]
pub fn parse_declaration_text(block: &str) -> IndexMap<Arc<str>, Arc<str>> {
let mut input_buf = ParserInput::new(block);
let mut input = CssParser::new(&mut input_buf);
let mut normal = CssAttr::new();
let mut important = CssAttr::new();
parse_declarations_into(&mut input, &mut normal, &mut important, false);
normal.0.extend(important.0);
normal.0
}
pub(crate) fn parse_declaration_splits(block: &str) -> (CssAttr, CssAttr) {
let mut input_buf = ParserInput::new(block);
let mut input = CssParser::new(&mut input_buf);
let mut normal = CssAttr::new();
let mut important = CssAttr::new();
parse_declarations_into(&mut input, &mut normal, &mut important, true);
(normal, important)
}
fn parse_declarations_into<'i, 't>(
input: &mut CssParser<'i, 't>,
normal: &mut CssAttr,
important: &mut CssAttr,
expand_place: bool,
) {
loop {
input.skip_whitespace();
if input.is_exhausted() {
return;
}
let name = match input.expect_ident() {
Ok(s) => s.clone(),
Err(_) => {
let _: Result<(), ParseError<()>> =
input.parse_until_after(Delimiter::Semicolon, |_| Ok(()));
continue;
}
};
let name = name.to_ascii_lowercase();
if input.expect_colon().is_err() {
let _: Result<(), ParseError<()>> =
input.parse_until_after(Delimiter::Semicolon, |_| Ok(()));
continue;
}
let value_start = input.position();
let _: Result<(), ParseError<()>> =
input.parse_until_before(Delimiter::Semicolon | Delimiter::Bang, |_| Ok(()));
let value_text = input.slice_from(value_start);
let is_important = input.try_parse(parse_important).is_ok();
let cleaned = clean_value_text(value_text);
if !name.is_empty() && !cleaned.is_empty() {
let target = if is_important {
&mut important.0
} else {
&mut normal.0
};
if expand_place && is_place_shorthand_name(&name) {
if let Some(pairs) = expand_place_shorthand(&name, &cleaned) {
for (prop, value) in pairs {
target.insert(Arc::from(prop), value);
}
}
} else if expand_place
&& is_alignment_longhand(&name)
&& !alignment_longhand_valid(&name, &cleaned)
{
} else {
target.insert(Arc::from(name.as_ref()), cleaned);
}
}
let _: Result<(), cssparser::BasicParseError<'_>> =
input.try_parse(|i| i.expect_semicolon().map(|_| ()));
}
}