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::media::{MediaQueryList, parse_media_query_list};
use super::{
properties::CssAttr,
selector::CssSelector,
values::{
alignment_longhand_valid, expand_grid_template, expand_place_shorthand,
is_alignment_longhand, is_list_style_type_keyword, is_place_shorthand_name,
},
};
#[derive(Debug, Clone)]
pub struct CssRuleEntry {
pub selector: CssSelector,
pub normal: CssAttr,
pub important: CssAttr,
pub media: Option<MediaQueryList>,
}
#[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.push_media(selector, normal, important, None);
}
pub fn push_media(
&mut self,
selector: CssSelector,
normal: CssAttr,
important: CssAttr,
media: Option<MediaQueryList>,
) {
self.1.take();
self.0.push(CssRuleEntry {
selector,
normal,
important,
media,
});
}
pub fn add(&mut self, selector: CssSelector, attr: CssAttr) {
self.push(selector, attr, 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 collected in state.rules.drain(..) {
match collected {
CollectedRule::Qualified(sel_text, normal, important) => {
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);
}
}
}
}
CollectedRule::Media(entry) => css.0.push(entry),
}
}
debug!("CSS parsed: {} rules", css.0.len());
css
}
}
struct CssParseState {
rules: Vec<CollectedRule>,
}
impl CssParseState {
fn new() -> Self {
Self { rules: vec![] }
}
}
enum CollectedRule {
Qualified(String, CssAttr, CssAttr),
Media(CssRuleEntry),
}
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();
let _: std::result::Result<(), ParseError<()>> =
input.parse_until_before(Delimiter::CurlyBracketBlock, |_| Ok(()));
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(CollectedRule::Qualified(prelude, normal, important));
Ok(())
}
}
enum AtRulePrelude {
Media(MediaQueryList),
Other,
}
impl<'i> AtRuleParser<'i> for CssParseState {
type Prelude = AtRulePrelude;
type AtRule = ();
type Error = cssparser::BasicParseError<'i>;
fn parse_prelude<'t>(
&mut self,
name: CowRcStr<'i>,
input: &mut CssParser<'i, 't>,
) -> std::result::Result<AtRulePrelude, ParseError<'i, Self::Error>> {
if name.eq_ignore_ascii_case("media") {
let start = input.position();
let _: std::result::Result<(), ParseError<()>> = input
.parse_until_before(Delimiter::Semicolon | Delimiter::CurlyBracketBlock, |_| {
Ok(())
});
let text = input.slice_from(start);
return match parse_media_query_list(text) {
Some(list) => Ok(AtRulePrelude::Media(list)),
None => Ok(AtRulePrelude::Other),
};
}
debug!("skip @{}", name);
while !input.is_exhausted() {
input.next().ok();
}
Ok(AtRulePrelude::Other)
}
fn parse_block<'t>(
&mut self,
prelude: AtRulePrelude,
_: &ParserState,
input: &mut CssParser<'i, 't>,
) -> std::result::Result<(), ParseError<'i, Self::Error>> {
let AtRulePrelude::Media(list) = prelude else {
while !input.is_exhausted() {
input.next().ok();
}
return Ok(());
};
let body = serialize_rule_body(input);
let mut sub = Parser::new(&body).parse();
for mut entry in sub.0.drain(..) {
if entry.media.is_some() {
warn!("Nested @media is not supported — dropping rule");
continue;
}
entry.media = Some(list.clone());
self.rules.push(CollectedRule::Media(entry));
}
Ok(())
}
fn rule_without_block(
&mut self,
_: AtRulePrelude,
_: &ParserState,
) -> std::result::Result<(), ()> {
Ok(())
}
}
fn serialize_rule_body<'i, 't>(input: &mut CssParser<'i, 't>) -> String {
let mut out = String::new();
serialize_tokens(input, &mut out);
out
}
fn serialize_tokens<'i, 't>(input: &mut CssParser<'i, 't>, out: &mut String) {
while !input.is_exhausted() {
let start = input.position();
let tok = match input.next_including_whitespace_and_comments() {
Ok(t) => t,
Err(_) => break,
};
let block: Option<(&str, &str)> = match tok {
cssparser::Token::CurlyBracketBlock => Some(("{", "}")),
cssparser::Token::ParenthesisBlock => Some(("(", ")")),
cssparser::Token::SquareBracketBlock => Some(("[", "]")),
cssparser::Token::Function(_) => Some((input.slice_from(start), ")")),
_ => None,
};
match block {
None => out.push_str(input.slice_from(start)),
Some((open, close)) => {
out.push_str(open);
let _: std::result::Result<(), ParseError<()>> =
input.parse_nested_block(|nested| {
serialize_tokens(nested, out);
Ok(())
});
out.push_str(close);
}
}
}
}
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
};
type Expansion = Vec<(&'static str, Arc<str>)>;
let expansion: Option<Option<Expansion>> = if !expand_place {
None
} else if is_place_shorthand_name(&name) {
Some(expand_place_shorthand(&name, &cleaned))
} else if name == "list-style" {
Some(expand_list_style_shorthand(&cleaned))
} else if name == "font" {
Some(expand_font_shorthand(&cleaned))
} else if name == "grid-template" {
expand_grid_template(&cleaned).map(Some)
} else {
None
};
match expansion {
Some(pairs) => {
for (prop, value) in pairs.unwrap_or_default() {
target.insert(Arc::from(prop), value);
}
}
None => {
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(|_| ()));
}
}
fn expand_list_style_shorthand(value: &str) -> Option<Vec<(&'static str, Arc<str>)>> {
let words: Vec<&str> = value.split_whitespace().collect();
if words.is_empty() {
return None;
}
let mut out = Vec::new();
for tok in words {
if is_list_style_type_keyword(tok) {
out.push(("list-style-type", Arc::from(tok)));
}
}
Some(out)
}
fn expand_font_shorthand(value: &str) -> Option<Vec<(&'static str, Arc<str>)>> {
let tokens: Vec<&str> = value.split_whitespace().collect();
let mut style: Option<&str> = None;
let mut weight: Option<&str> = None;
let mut i = 0usize;
while let Some(&tok) = tokens.get(i) {
match tok.to_ascii_lowercase().as_str() {
"italic" | "oblique" => style = Some(tok),
"bold" | "bolder" | "lighter" => weight = Some(tok),
"normal" | "small-caps" => {}
_ => break,
}
i += 1;
}
let size_tok = tokens.get(i)?;
let (size, mut line_height) = match size_tok.split_once('/') {
Some((s, lh)) => (s.to_string(), Some(lh.to_string())),
None => (size_tok.to_string(), None),
};
let looks_numeric = size
.chars()
.next()
.is_some_and(|c| c.is_ascii_digit() || c == '.');
if !looks_numeric {
return None;
}
i += 1;
if line_height.is_none()
&& let Some(&tok) = tokens.get(i)
&& let Some(lh) = tok.strip_prefix('/')
{
if lh.is_empty() {
return None;
}
line_height = Some(lh.to_string());
i += 1;
}
tokens.get(i)?;
let mut out = Vec::new();
if let Some(s) = style {
out.push(("font-style", Arc::from(s)));
}
if let Some(w) = weight {
out.push(("font-weight", Arc::from(w)));
}
out.push(("font-size", Arc::from(size)));
if let Some(lh) = line_height {
out.push(("line-height", Arc::from(lh)));
}
Some(out)
}