lv-tui 0.1.1

A reactive TUI framework for Rust, inspired by Textual and React
Documentation
use crate::style::{Border, Color, Layout, Length, Style};

/// 选择器
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Selector {
    /// 类型选择器:`Button`
    Type(String),
    /// class 选择器:`.card`
    Class(String),
    /// id 选择器:`#header`
    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()
    }

    /// 根据组件的类型名/id/class 解析匹配样式
    pub fn resolve(&self, type_name: &str, id: Option<&str>, class: Option<&str>) -> Style {
        let mut resolved = Style::default();

        // 优先级:type < class < id
        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 },
        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 },
        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();
            // Skip // line comments
            if self.peek_char() == Some('/') {
                // Peek next
                let mut iter = self.chars.clone();
                iter.next();
                if iter.next() == Some('/') {
                    // Skip comment until newline
                    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()));
    }
}