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();
let mut remaining = s.trim();
while !remaining.is_empty() {
let mut paren_depth: i32 = 0;
let colon_pos = {
let mut pos = None;
for (i, ch) in remaining.char_indices() {
match ch {
'(' => paren_depth += 1,
')' => paren_depth = (paren_depth - 1).max(0),
':' if paren_depth == 0 => {
pos = Some(i);
break;
}
_ => {}
}
}
pos
};
let colon_pos = match colon_pos {
Some(p) => p,
None => break,
};
let property = remaining[..colon_pos].trim().to_lowercase();
remaining = remaining[colon_pos + 1..].trim_start();
let (value, rest) = {
let mut paren_depth: i32 = 0;
let sc_pos = {
let mut pos = None;
for (i, ch) in remaining.char_indices() {
match ch {
'(' => paren_depth += 1,
')' => paren_depth = (paren_depth - 1).max(0),
';' if paren_depth == 0 => {
pos = Some(i);
break;
}
_ => {}
}
}
pos
};
if let Some(p) = sc_pos {
(remaining[..p].trim().to_string(), remaining[p + 1..].trim())
} else {
(remaining.trim().to_string(), "")
}
};
if !property.is_empty() {
map.insert(property, value);
}
remaining = rest;
}
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"));
}
}