use crate::style::{Border, Color, Layout, Length, Style};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Selector {
Type(String),
Class(String),
Id(String),
}
#[derive(Debug, Clone)]
pub struct StyleRule {
pub selector: Selector,
pub style: Style,
}
#[derive(Debug, Clone, Default)]
pub struct StyleSheet {
pub rules: Vec<StyleRule>,
}
impl StyleSheet {
pub fn parse(input: &str) -> Result<Self, String> {
let mut parser = Parser::new(input);
parser.parse()
}
pub fn resolve(&self, type_name: &str, id: Option<&str>, class: Option<&str>) -> Style {
let mut resolved = Style::default();
for rule in &self.rules {
match &rule.selector {
Selector::Type(name) if name == type_name => {
resolved = merge_styles(resolved, &rule.style);
}
Selector::Class(name) if class == Some(name) => {
resolved = merge_styles(resolved, &rule.style);
}
Selector::Id(name) if id == Some(name) => {
resolved = merge_styles(resolved, &rule.style);
}
_ => {}
}
}
resolved
}
}
pub fn inherit_style(parent: &Style, child: &Style) -> Style {
Style {
fg: child.fg.or(parent.fg),
bg: child.bg.or(parent.bg),
bold: child.bold || parent.bold,
italic: child.italic || parent.italic,
underline: child.underline || parent.underline,
width: if child.width != Length::Auto { child.width } else { parent.width },
height: if child.height != Length::Auto { child.height } else { parent.height },
padding: if child.padding != crate::geom::Insets::ZERO { child.padding } else { parent.padding },
margin: if child.margin != crate::geom::Insets::ZERO { child.margin } else { parent.margin },
layout: if child.layout != Layout::None { child.layout } else { parent.layout },
gap: if child.gap != 0 { child.gap } else { parent.gap },
flex_grow: if child.flex_grow != 0 { child.flex_grow } else { parent.flex_grow },
flex_shrink: child.flex_shrink && parent.flex_shrink,
border: if child.border != Border::None { child.border } else { parent.border },
}
}
pub fn merge_styles(base: Style, override_: &Style) -> Style {
Style {
fg: override_.fg.or(base.fg),
bg: override_.bg.or(base.bg),
bold: override_.bold || base.bold,
italic: override_.italic || base.italic,
underline: override_.underline || base.underline,
width: if override_.width != Length::Auto { override_.width } else { base.width },
height: if override_.height != Length::Auto { override_.height } else { base.height },
padding: if override_.padding != crate::geom::Insets::ZERO { override_.padding } else { base.padding },
margin: if override_.margin != crate::geom::Insets::ZERO { override_.margin } else { base.margin },
layout: if override_.layout != Layout::None { override_.layout } else { base.layout },
gap: if override_.gap != 0 { override_.gap } else { base.gap },
flex_grow: if override_.flex_grow != 0 { override_.flex_grow } else { base.flex_grow },
flex_shrink: override_.flex_shrink && base.flex_shrink,
border: if override_.border != Border::None { override_.border } else { base.border },
..base
}
}
struct Parser<'a> {
chars: std::iter::Peekable<std::str::Chars<'a>>,
pos: usize,
}
impl<'a> Parser<'a> {
fn new(input: &'a str) -> Self {
Self {
chars: input.chars().peekable(),
pos: 0,
}
}
fn parse(&mut self) -> Result<StyleSheet, String> {
let mut rules = Vec::new();
self.skip_whitespace_and_comments();
while self.chars.peek().is_some() {
rules.push(self.parse_rule()?);
self.skip_whitespace_and_comments();
}
Ok(StyleSheet { rules })
}
fn parse_rule(&mut self) -> Result<StyleRule, String> {
let selector = self.parse_selector()?;
self.skip_whitespace();
self.expect('{')?;
let style = self.parse_declarations()?;
self.expect('}')?;
Ok(StyleRule { selector, style })
}
fn parse_selector(&mut self) -> Result<Selector, String> {
let next = self.peek_char().ok_or("expected selector")?;
match next {
'.' => {
self.advance();
let name = self.parse_ident()?;
Ok(Selector::Class(name))
}
'#' => {
self.advance();
let name = self.parse_ident()?;
Ok(Selector::Id(name))
}
c if c.is_alphabetic() || c == '_' || c == '-' => {
let name = self.parse_ident()?;
Ok(Selector::Type(name))
}
_ => Err(format!("unexpected char '{}' in selector", next)),
}
}
fn parse_declarations(&mut self) -> Result<Style, String> {
let mut style = Style::default();
loop {
self.skip_whitespace();
if self.peek_char() == Some('}') || self.peek_char().is_none() {
break;
}
let prop = self.parse_ident()?;
self.skip_whitespace();
self.expect(':')?;
self.skip_whitespace();
let value = self.parse_value(&prop)?;
self.apply_property(&mut style, &prop, &value);
self.skip_whitespace();
if self.peek_char() == Some(';') {
self.advance();
}
}
Ok(style)
}
fn parse_value(&mut self, _prop: &str) -> Result<String, String> {
let mut val = String::new();
while let Some(&c) = self.chars.peek() {
if c == ';' || c == '}' || c == '\n' {
break;
}
val.push(c);
self.advance();
}
Ok(val.trim().to_string())
}
fn apply_property(&self, style: &mut Style, prop: &str, value: &str) {
match prop {
"fg" | "color" => {
if let Some(c) = parse_color(value) {
style.fg = Some(c);
}
}
"bg" | "background" => {
if let Some(c) = parse_color(value) {
style.bg = Some(c);
}
}
"bold" => style.bold = value == "true",
"italic" => style.italic = value == "true",
"underline" => style.underline = value == "true",
"padding" => {
if let Ok(n) = value.parse::<u16>() {
style.padding = crate::geom::Insets::all(n);
}
}
"margin" => {
if let Ok(n) = value.parse::<u16>() {
style.margin = crate::geom::Insets::all(n);
}
}
"gap" => {
if let Ok(n) = value.parse::<u16>() {
style.gap = n;
}
}
"width" => style.width = parse_length(value),
"height" => style.height = parse_length(value),
"layout" => {
style.layout = match value {
"vertical" | "column" => Layout::Vertical,
"horizontal" | "row" => Layout::Horizontal,
_ => Layout::None,
}
}
"border" => {
style.border = match value {
"plain" => Border::Plain,
"rounded" => Border::Rounded,
"double" => Border::Double,
_ => Border::None,
}
}
_ => {} }
}
fn parse_ident(&mut self) -> Result<String, String> {
let mut ident = String::new();
while let Some(&c) = self.chars.peek() {
if c.is_alphanumeric() || c == '_' || c == '-' {
ident.push(c);
self.advance();
} else {
break;
}
}
if ident.is_empty() {
Err("expected identifier".into())
} else {
Ok(ident)
}
}
fn skip_whitespace(&mut self) {
while let Some(&c) = self.chars.peek() {
if c.is_whitespace() {
self.advance();
} else {
break;
}
}
}
fn skip_whitespace_and_comments(&mut self) {
loop {
self.skip_whitespace();
if self.peek_char() == Some('/') {
let mut iter = self.chars.clone();
iter.next();
if iter.next() == Some('/') {
while let Some(&c) = self.chars.peek() {
self.advance();
if c == '\n' {
break;
}
}
continue;
}
}
break;
}
}
fn peek_char(&mut self) -> Option<char> {
self.chars.peek().copied()
}
fn advance(&mut self) -> Option<char> {
self.pos += 1;
self.chars.next()
}
fn expect(&mut self, expected: char) -> Result<(), String> {
match self.chars.peek() {
Some(&c) if c == expected => {
self.advance();
Ok(())
}
Some(&c) => Err(format!("expected '{}', found '{}'", expected, c)),
None => Err(format!("expected '{}', found EOF", expected)),
}
}
}
fn parse_color(s: &str) -> Option<Color> {
match s {
"black" => Some(Color::Black),
"red" => Some(Color::Red),
"green" => Some(Color::Green),
"yellow" => Some(Color::Yellow),
"blue" => Some(Color::Blue),
"magenta" => Some(Color::Magenta),
"cyan" => Some(Color::Cyan),
"white" => Some(Color::White),
"gray" | "grey" => Some(Color::Gray),
_ => None,
}
}
fn parse_length(s: &str) -> Length {
if s == "auto" {
return Length::Auto;
}
if let Some(pct) = s.strip_suffix('%') {
if let Ok(n) = pct.parse::<u16>() {
return Length::Percent(n);
}
}
if let Some(frac) = s.strip_suffix("fr") {
if let Ok(n) = frac.parse::<u16>() {
return Length::Fraction(n);
}
}
if let Ok(n) = s.parse::<u16>() {
return Length::Fixed(n);
}
Length::Auto
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple() {
let css = "Counter { fg: green; padding: 1; }";
let sheet = StyleSheet::parse(css).unwrap();
assert_eq!(sheet.rules.len(), 1);
assert_eq!(sheet.rules[0].selector, Selector::Type("Counter".into()));
assert_eq!(sheet.rules[0].style.fg, Some(Color::Green));
assert_eq!(sheet.rules[0].style.padding, crate::geom::Insets::all(1));
}
#[test]
fn test_parse_class_id() {
let css = ".card { border: rounded; } #header { bold: true; }";
let sheet = StyleSheet::parse(css).unwrap();
assert_eq!(sheet.rules.len(), 2);
assert_eq!(sheet.rules[0].selector, Selector::Class("card".into()));
assert_eq!(sheet.rules[1].selector, Selector::Id("header".into()));
}
}