use crate::{CssRule, Declaration, StyleSheet, MediaQuery, MediaRule, KeyframesRule, Keyframe};
pub fn parse_css(css: &str) -> Result<StyleSheet, String> {
Ok(parse_css_simple(css))
}
pub fn parse_css_simple(css: &str) -> StyleSheet {
let mut sheet = StyleSheet::default();
let mut remaining = css;
while let Some(open_pos) = remaining.find('{') {
let before = remaining[..open_pos].trim();
remaining = &remaining[open_pos + 1..];
let close_pos = match find_matching_brace(remaining) {
Some(p) => p,
None => break,
};
let block_content = remaining[..close_pos].trim();
remaining = &remaining[close_pos + 1..];
if before.starts_with("@media") {
let query_str = before[6..].trim();
let query = MediaQuery::parse(query_str);
let rules = parse_nested_rules(block_content);
sheet.media_rules.push(MediaRule { query, rules });
continue;
}
if before.starts_with("@keyframes") {
let name = before[10..].trim().to_string();
let keyframes = parse_keyframes(block_content);
sheet.keyframes_rules.push(KeyframesRule { name, keyframes });
continue;
}
if before.starts_with("@-webkit-keyframes") {
let name = before[18..].trim().to_string();
let keyframes = parse_keyframes(block_content);
sheet.keyframes_rules.push(KeyframesRule { name, keyframes });
continue;
}
if before.starts_with('@') {
continue;
}
if before.is_empty() {
continue;
}
let selectors: Vec<String> = before.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
let declarations = parse_declarations(block_content);
if !selectors.is_empty() {
sheet.rules.push(CssRule { selectors, declarations });
}
}
sheet
}
fn parse_nested_rules(content: &str) -> Vec<CssRule> {
let mut rules = Vec::new();
let mut remaining = content;
while let Some(open_pos) = remaining.find('{') {
let before = remaining[..open_pos].trim();
remaining = &remaining[open_pos + 1..];
let close_pos = match find_matching_brace(remaining) {
Some(p) => p,
None => break,
};
let decl_str = remaining[..close_pos].trim();
remaining = &remaining[close_pos + 1..];
if before.starts_with('@') {
continue;
}
if before.is_empty() {
continue;
}
let selectors: Vec<String> = before.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
let declarations = parse_declarations(decl_str);
if !selectors.is_empty() {
rules.push(CssRule { selectors, declarations });
}
}
rules
}
fn parse_keyframes(content: &str) -> Vec<Keyframe> {
let mut keyframes = Vec::new();
let mut remaining = content;
while let Some(open_pos) = remaining.find('{') {
let before = remaining[..open_pos].trim();
remaining = &remaining[open_pos + 1..];
let close_pos = match find_matching_brace(remaining) {
Some(p) => p,
None => break,
};
let decl_str = remaining[..close_pos].trim();
remaining = &remaining[close_pos + 1..];
let selectors: Vec<&str> = before.split(',').map(|s| s.trim()).collect();
let declarations = parse_declarations(decl_str);
for selector in selectors {
if !selector.is_empty() {
keyframes.push(Keyframe {
selector: selector.to_string(),
declarations: declarations.clone(),
});
}
}
}
keyframes.sort_by(|a, b| {
a.percentage().partial_cmp(&b.percentage()).unwrap_or(std::cmp::Ordering::Equal)
});
keyframes
}
fn find_matching_brace(s: &str) -> Option<usize> {
let mut depth = 1i32;
for (i, c) in s.char_indices() {
match c {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 { return Some(i); }
}
_ => {}
}
}
None
}
fn parse_declarations(s: &str) -> Vec<Declaration> {
let mut result = Vec::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() {
result.push(Declaration { property, value });
}
remaining = rest;
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple() {
let sheet = parse_css_simple(".foo { color: red; font-size: 16px; } .bar { margin: 10px; }");
assert_eq!(sheet.rules.len(), 2);
assert_eq!(sheet.rules[0].selectors[0], ".foo");
assert_eq!(sheet.rules[0].declarations[0].property, "color");
}
#[test]
fn test_complex_selector() {
let sheet = parse_css_simple("div.container > p.highlight { color: blue; }");
assert_eq!(sheet.rules.len(), 1);
assert_eq!(sheet.rules[0].declarations[0].value, "blue");
}
#[test]
fn test_multiple_selectors() {
let sheet = parse_css_simple("h1, h2, h3 { font-weight: bold; }");
assert_eq!(sheet.rules[0].selectors.len(), 3);
}
#[test]
fn test_at_rule_skipped() {
let sheet = parse_css_simple("@media screen { .a { color: red; } } .b { color: blue; }");
assert_eq!(sheet.rules.len(), 1);
assert_eq!(sheet.rules[0].selectors[0], ".b");
}
#[test]
fn test_stylesheet_compute() {
let sheet = parse_css_simple(".btn { color: red; font-size: 14px; } .btn-primary { color: blue; }");
let map = sheet.compute(&["btn".into(), "btn-primary".into()], "button");
assert_eq!(map.get("color").unwrap(), "blue");
assert_eq!(map.get("font-size").unwrap(), "14px");
}
#[test]
fn test_empty_input() {
let sheet = parse_css_simple("");
assert_eq!(sheet.rules.len(), 0);
}
#[test]
fn test_nested_braces() {
let sheet = parse_css_simple(".a { x: 1; } .b { y: 2; }");
assert_eq!(sheet.rules.len(), 2);
}
#[test]
fn test_media_rule_parsing() {
let sheet = parse_css_simple(
"@media screen and (min-width: 768px) { .container { width: 750px; } }"
);
assert_eq!(sheet.media_rules.len(), 1);
assert_eq!(sheet.media_rules[0].query.media_type, "screen");
assert_eq!(sheet.media_rules[0].rules.len(), 1);
assert_eq!(sheet.media_rules[0].rules[0].selectors[0], ".container");
}
#[test]
fn test_keyframes_parsing() {
let sheet = parse_css_simple(
"@keyframes fade { from { opacity: 0; } to { opacity: 1; } }"
);
assert_eq!(sheet.keyframes_rules.len(), 1);
assert_eq!(sheet.keyframes_rules[0].name, "fade");
assert_eq!(sheet.keyframes_rules[0].keyframes.len(), 2);
assert_eq!(sheet.keyframes_rules[0].keyframes[0].selector, "from");
assert_eq!(sheet.keyframes_rules[0].keyframes[1].selector, "to");
}
#[test]
fn test_keyframes_percentage() {
let sheet = parse_css_simple(
"@keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.1); } 100% { transform: scale(1); } }"
);
assert_eq!(sheet.keyframes_rules[0].keyframes.len(), 3);
assert!((sheet.keyframes_rules[0].keyframes[1].percentage() - 0.5).abs() < 0.001);
}
#[test]
fn test_media_query_matching() {
let query = MediaQuery::parse("screen and (min-width: 768px)");
assert!(query.matches(1024.0, 768.0)); assert!(!query.matches(480.0, 640.0)); }
#[test]
fn test_compute_with_media() {
let sheet = parse_css_simple(
".box { width: 100px; } @media screen and (min-width: 768px) { .box { width: 200px; } }"
);
let small_screen = sheet.compute_with_media(&["box".into()], "div", 480.0, 640.0);
assert_eq!(small_screen.get("width").unwrap(), "100px");
let large_screen = sheet.compute_with_media(&["box".into()], "div", 1024.0, 768.0);
assert_eq!(large_screen.get("width").unwrap(), "200px");
}
}