use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum CSSNode {
Stylesheet(Vec<CSSRule>),
Rule(CSSRule),
Declaration(CSSDeclaration),
AtRule(CSSAtRule),
Comment(String),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CSSRule {
pub selector: String,
pub declarations: Vec<CSSDeclaration>,
pub nested_rules: Vec<CSSRule>,
pub media_query: Option<String>,
pub specificity: u32,
pub position: Option<SourcePosition>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CSSDeclaration {
pub property: String,
pub value: String,
pub important: bool,
pub position: Option<SourcePosition>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CSSAtRule {
pub name: String,
pub params: String,
pub body: Vec<CSSNode>,
pub position: Option<SourcePosition>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SourcePosition {
pub line: usize,
pub column: usize,
pub source: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SelectorComponent {
Class(String),
Id(String),
Element(String),
Attribute(AttributeSelector),
PseudoClass(String),
PseudoElement(String),
Universal,
Combinator(CombinatorType),
Group(Vec<SelectorComponent>),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AttributeSelector {
pub name: String,
pub operator: AttributeOperator,
pub value: Option<String>,
pub case_sensitive: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum AttributeOperator {
Exists,
Equals,
ContainsWord,
StartsWith,
StartsWithPrefix,
EndsWith,
Contains,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum CombinatorType {
Descendant,
Child,
AdjacentSibling,
GeneralSibling,
}
impl CSSRule {
pub fn calculate_specificity(&self) -> u32 {
let mut specificity = 0u32;
let id_count = self.selector.matches('#').count();
specificity += (id_count as u32) * 100;
let class_count = self.selector.matches('.').count();
let attribute_count = self.selector.matches('[').count();
let pseudo_class_count =
self.selector.matches(':').count() - self.selector.matches("::").count();
specificity += ((class_count + attribute_count + pseudo_class_count) as u32) * 10;
let element_count = self
.selector
.split_whitespace()
.filter(|s| {
!s.starts_with('.')
&& !s.starts_with('#')
&& !s.starts_with('[')
&& !s.starts_with(':')
})
.count();
specificity += element_count as u32;
specificity
}
pub fn matches_selector(&self, target_selector: &str) -> bool {
self.selector == target_selector
}
pub fn add_declaration(&mut self, property: String, value: String, important: bool) {
let declaration = CSSDeclaration {
property,
value,
important,
position: None,
};
self.declarations.push(declaration);
}
pub fn remove_declaration(&mut self, property: &str) {
self.declarations.retain(|decl| decl.property != property);
}
pub fn get_declaration(&self, property: &str) -> Option<&CSSDeclaration> {
self.declarations
.iter()
.find(|decl| decl.property == property)
}
pub fn has_property(&self, property: &str) -> bool {
self.declarations
.iter()
.any(|decl| decl.property == property)
}
}
impl CSSDeclaration {
pub fn new(property: String, value: String) -> Self {
Self {
property,
value,
important: false,
position: None,
}
}
pub fn new_important(property: String, value: String) -> Self {
Self {
property,
value,
important: true,
position: None,
}
}
pub fn set_important(&mut self) {
self.important = true;
}
pub fn is_important(&self) -> bool {
self.important
}
}
impl CSSAtRule {
pub fn new(name: String, params: String) -> Self {
Self {
name,
params,
body: Vec::new(),
position: None,
}
}
pub fn add_rule(&mut self, rule: CSSRule) {
self.body.push(CSSNode::Rule(rule));
}
pub fn add_declaration(&mut self, declaration: CSSDeclaration) {
self.body.push(CSSNode::Declaration(declaration));
}
}
impl CSSNode {
pub fn get_rules(&self) -> Vec<&CSSRule> {
match self {
CSSNode::Stylesheet(rules) => rules.iter().collect(),
CSSNode::Rule(rule) => vec![rule],
_ => Vec::new(),
}
}
pub fn get_declarations(&self) -> Vec<&CSSDeclaration> {
match self {
CSSNode::Rule(rule) => rule.declarations.iter().collect(),
CSSNode::Declaration(decl) => vec![decl],
_ => Vec::new(),
}
}
pub fn find_rules_by_selector(&self, selector: &str) -> Vec<&CSSRule> {
self.get_rules()
.into_iter()
.filter(|rule| rule.matches_selector(selector))
.collect()
}
pub fn find_rules_by_property(&self, property: &str) -> Vec<&CSSRule> {
self.get_rules()
.into_iter()
.filter(|rule| rule.has_property(property))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_css_rule_creation() {
let rule = CSSRule {
selector: ".test".to_string(),
declarations: vec![
CSSDeclaration::new("color".to_string(), "red".to_string()),
CSSDeclaration::new("font-size".to_string(), "16px".to_string()),
],
nested_rules: Vec::new(),
media_query: None,
specificity: 0,
position: None,
};
assert_eq!(rule.selector, ".test");
assert_eq!(rule.declarations.len(), 2);
assert!(rule.has_property("color"));
assert!(!rule.has_property("background"));
}
#[test]
fn test_specificity_calculation() {
let rule = CSSRule {
selector: "#id .class div".to_string(),
declarations: Vec::new(),
nested_rules: Vec::new(),
media_query: None,
specificity: 0,
position: None,
};
let specificity = rule.calculate_specificity();
assert_eq!(specificity, 111);
}
#[test]
fn test_declaration_creation() {
let decl = CSSDeclaration::new_important("color".to_string(), "red".to_string());
assert_eq!(decl.property, "color");
assert_eq!(decl.value, "red");
assert!(decl.is_important());
}
#[test]
fn test_at_rule_creation() {
let mut at_rule = CSSAtRule::new("media".to_string(), "(max-width: 768px)".to_string());
at_rule.add_rule(CSSRule {
selector: ".mobile".to_string(),
declarations: vec![CSSDeclaration::new(
"display".to_string(),
"block".to_string(),
)],
nested_rules: Vec::new(),
media_query: None,
specificity: 0,
position: None,
});
assert_eq!(at_rule.name, "media");
assert_eq!(at_rule.params, "(max-width: 768px)");
assert_eq!(at_rule.body.len(), 1);
}
}