use std::collections::HashMap;
use anyhow::{Context, Result};
#[derive(Debug, Default, Clone)]
pub struct StyleSheet {
rules: HashMap<String, HashMap<String, String>>,
}
impl StyleSheet {
pub fn parse(css: &str) -> Result<Self> {
let mut rules: HashMap<String, HashMap<String, String>> = HashMap::new();
let mut chars = css.chars().peekable();
while let Some(&c) = chars.peek() {
if c.is_whitespace() {
chars.next();
continue;
}
if c == '/' {
chars.next();
if chars.peek() == Some(&'*') {
chars.next();
loop {
match chars.next() {
Some('*') if chars.peek() == Some(&'/') => {
chars.next();
break;
}
None => break,
_ => {}
}
}
}
continue;
}
let selector: String = chars.by_ref().take_while(|&c| c != '{').collect();
let selector = selector.trim().to_string();
let block: String = chars.by_ref().take_while(|&c| c != '}').collect();
if selector.is_empty() {
continue;
}
let mut props = HashMap::new();
for declaration in block.split(';') {
let declaration = declaration.trim();
if declaration.is_empty() {
continue;
}
if let Some((key, value)) = declaration.split_once(':') {
props.insert(
key.trim().to_string(),
value.trim().to_string(),
);
}
}
for sel in selector.split(',') {
let sel = sel.trim().trim_start_matches('.').to_string();
rules
.entry(sel)
.or_default()
.extend(props.clone());
}
}
Ok(Self { rules })
}
pub fn from_file(path: &str) -> Result<Self> {
let css = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read CSS file: {}", path))?;
Self::parse(&css)
}
pub fn resolve(&self, class: &str) -> Option<&HashMap<String, String>> {
self.rules.get(class)
}
pub fn get(&self, class: &str, property: &str) -> Option<&str> {
self.rules.get(class)?.get(property).map(|s| s.as_str())
}
pub fn merge(&mut self, other: StyleSheet) {
for (class, props) in other.rules {
self.rules.entry(class).or_default().extend(props);
}
}
pub fn rule_count(&self) -> usize {
self.rules.len()
}
}
pub const MAIN_CSS_EXAMPLE: &str = r#"
/* ── Layout ─────────────────────────────────────── */
.screen {
flex-direction: column;
align-items: center;
padding: 24px;
background: #f8f9fa;
}
/* ── Typography ──────────────────────────────────── */
.title {
font-size: 28px;
font-weight: bold;
color: #1a1a2e;
margin-bottom: 16px;
}
.label {
font-size: 16px;
color: #555;
margin-bottom: 8px;
}
/* ── Buttons ─────────────────────────────────────── */
.primary-btn {
background: #4CAF50;
color: white;
padding: 12px 24px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
margin-top: 12px;
}
.link-btn {
background: transparent;
color: #4CAF50;
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
margin-top: 8px;
}
.danger-btn {
background: #e53935;
color: white;
padding: 12px 24px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
margin-top: 12px;
}
/* ── Inputs ──────────────────────────────────────── */
.text-input {
background: white;
border: 1.5px solid #ddd;
border-radius: 8px;
padding: 12px 16px;
font-size: 16px;
width: 100%;
margin-top: 16px;
}
/* ── Avatar ──────────────────────────────────────── */
.avatar {
width: 80px;
height: 80px;
border-radius: 40px;
margin-bottom: 16px;
border: 3px solid #4CAF50;
}
"#;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_basic_rule() {
let sheet = StyleSheet::parse(".title { font-size: 24px; color: red; }").unwrap();
assert_eq!(sheet.get("title", "font-size"), Some("24px"));
assert_eq!(sheet.get("title", "color"), Some("red"));
}
#[test]
fn parse_multiple_selectors() {
let sheet = StyleSheet::parse(".a, .b { color: blue; }").unwrap();
assert_eq!(sheet.get("a", "color"), Some("blue"));
assert_eq!(sheet.get("b", "color"), Some("blue"));
}
#[test]
fn parse_main_css() {
let sheet = StyleSheet::parse(MAIN_CSS_EXAMPLE).unwrap();
assert!(sheet.rule_count() > 0);
assert_eq!(sheet.get("primary-btn", "background"), Some("#4CAF50"));
assert_eq!(sheet.get("danger-btn", "background"), Some("#e53935"));
}
}