use crate::{CssRule, Declaration, StyleSheet};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct CssStyleSheet {
pub rules: Vec<CssStyleRule>,
pub disabled: bool,
pub owner_id: Option<String>,
}
impl CssStyleSheet {
pub fn from_parsed(sheet: &StyleSheet) -> Self {
CssStyleSheet {
rules: sheet.rules.iter().map(CssStyleRule::from).collect(),
disabled: false,
owner_id: None,
}
}
pub fn from_css(css: &str) -> Self {
let sheet = StyleSheet::parse(css).unwrap_or_default();
Self::from_parsed(&sheet)
}
pub fn insert_rule(&mut self, rule: &str, index: usize) -> Result<(), String> {
if let Some((selector_part, body)) = rule.split_once('{') {
let selector = selector_part.trim();
let decl_str = body.trim_end_matches('}').trim();
let declarations = parse_decl_string(decl_str);
let css_rule = CssStyleRule {
selector_text: selector.to_string(),
style: CssStyleDeclaration { properties: declarations },
};
if index <= self.rules.len() {
self.rules.insert(index, css_rule);
} else {
self.rules.push(css_rule);
}
Ok(())
} else {
Err("Invalid rule format".to_string())
}
}
pub fn delete_rule(&mut self, index: usize) {
if index < self.rules.len() {
self.rules.remove(index);
}
}
pub fn to_js_json(&self) -> String {
let rules_json: Vec<String> = self.rules.iter().map(|r| {
format!(
r#"{{"selectorText":"{}","style":{}}}"#,
r.selector_text.replace('"', r#"\""#),
r.style.to_js_json()
)
}).collect();
format!(r#"{{"rules":[{}],"disabled":{}}}"#, rules_json.join(","), self.disabled)
}
}
#[derive(Debug, Clone)]
pub struct CssStyleRule {
pub selector_text: String,
pub style: CssStyleDeclaration,
}
impl From<&CssRule> for CssStyleRule {
fn from(rule: &CssRule) -> Self {
CssStyleRule {
selector_text: rule.selectors.join(", "),
style: CssStyleDeclaration::from(&rule.declarations),
}
}
}
#[derive(Debug, Clone)]
pub struct CssStyleDeclaration {
pub properties: HashMap<String, String>,
}
impl CssStyleDeclaration {
pub fn new() -> Self { CssStyleDeclaration { properties: HashMap::new() } }
pub fn get_property_value(&self, prop: &str) -> Option<&str> {
self.properties.get(prop).map(|s| s.as_str())
}
pub fn set_property(&mut self, prop: &str, value: &str) {
self.properties.insert(prop.to_string(), value.to_string());
}
pub fn remove_property(&mut self, prop: &str) {
self.properties.remove(prop);
}
pub fn to_js_json(&self) -> String {
let entries: Vec<String> = self.properties.iter()
.map(|(k, v)| format!(r#""{}":"{}""#, k.replace('"', r#"\""#), v.replace('"', r#"\""#)))
.collect();
format!("{{{}}}", entries.join(","))
}
}
impl From<&Vec<Declaration>> for CssStyleDeclaration {
fn from(decls: &Vec<Declaration>) -> Self {
let mut properties = HashMap::new();
for d in decls {
properties.insert(d.property.clone(), d.value.clone());
}
CssStyleDeclaration { properties }
}
}
pub struct Css;
impl Css {
pub fn escape(ident: &str) -> String {
ident.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\'', "\\'")
}
pub fn supports(property: &str, value: &str) -> bool {
!property.is_empty() && !value.is_empty()
}
}
fn parse_decl_string(s: &str) -> HashMap<String, String> {
let mut map = HashMap::new();
for part in s.split(';') {
let part = part.trim();
if part.is_empty() { continue; }
if let Some((prop, val)) = part.split_once(':') {
map.insert(prop.trim().to_lowercase(), val.trim().to_string());
}
}
map
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_css_style_sheet_from_css() {
let sheet = CssStyleSheet::from_css(".foo { color: red; } .bar { margin: 10px; }");
assert_eq!(sheet.rules.len(), 2);
assert_eq!(sheet.rules[0].selector_text, ".foo");
}
#[test]
fn test_insert_rule() {
let mut sheet = CssStyleSheet::from_css("");
sheet.insert_rule(".btn { color: blue; }", 0).unwrap();
assert_eq!(sheet.rules.len(), 1);
assert_eq!(sheet.rules[0].style.get_property_value("color").unwrap(), "blue");
}
#[test]
fn test_delete_rule() {
let mut sheet = CssStyleSheet::from_css(".a { x:1; } .b { y:2; }");
sheet.delete_rule(0);
assert_eq!(sheet.rules.len(), 1);
}
#[test]
fn test_style_declaration() {
let mut decl = CssStyleDeclaration::new();
decl.set_property("color", "red");
assert_eq!(decl.get_property_value("color").unwrap(), "red");
decl.remove_property("color");
assert!(decl.get_property_value("color").is_none());
}
#[test]
fn test_to_js_json() {
let sheet = CssStyleSheet::from_css(".btn { color: red; font-size: 14px; }");
let json = sheet.to_js_json();
assert!(json.contains("color"));
assert!(json.contains("red"));
assert!(json.contains("selectorText"));
}
}