Skip to main content

lv_tui/
style_parser.rs

1use crate::style::{Border, Color, Layout, Length, Style};
2
3/// A CSS-like selector that matches components by type, class, or id.
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum Selector {
6    /// Type selector, e.g. `Button` matches `component.type_name()`.
7    Type(String),
8    /// Class selector, e.g. `.card` matches `component.class()`.
9    Class(String),
10    /// Id selector, e.g. `#header` matches `component.id()`.
11    Id(String),
12}
13
14/// A single style rule pairing a selector with its declarations.
15#[derive(Debug, Clone)]
16pub struct StyleRule {
17    /// The selector that determines which components this rule applies to.
18    pub selector: Selector,
19    /// The style properties to apply.
20    pub style: Style,
21}
22
23/// A parsed stylesheet containing an ordered list of rules.
24///
25/// Use [`StyleSheet::parse`] to parse a CSS-like string, then
26/// [`StyleSheet::resolve`] to compute the effective style for a component.
27#[derive(Debug, Clone, Default)]
28pub struct StyleSheet {
29    /// Rules in source order (higher-index rules override earlier ones
30    /// for the same specificity).
31    pub rules: Vec<StyleRule>,
32}
33
34impl StyleSheet {
35    /// Parses a stylesheet string into a [`StyleSheet`].
36    ///
37    /// Syntax is CSS-like but simplified:
38    /// ```text
39    /// Counter { fg: green; padding: 1; border: rounded; }
40    /// .card { layout: vertical; }
41    /// #header { bold: true; }
42    /// ```
43    pub fn parse(input: &str) -> Result<Self, String> {
44        let mut parser = Parser::new(input);
45        parser.parse()
46    }
47
48    /// Resolves the effective style for a component by matching its type name,
49    /// optional id, and optional class against all rules.
50    ///
51    /// Specificity order: type < class < id. Later rules override earlier ones
52    /// at the same specificity.
53    pub fn resolve(&self, type_name: &str, id: Option<&str>, class: Option<&str>) -> Style {
54        let mut resolved = Style::default();
55
56        // 优先级:type < class < id
57        for rule in &self.rules {
58            match &rule.selector {
59                Selector::Type(name) if name == type_name => {
60                    resolved = merge_styles(resolved, &rule.style);
61                }
62                Selector::Class(name) if class == Some(name) => {
63                    resolved = merge_styles(resolved, &rule.style);
64                }
65                Selector::Id(name) if id == Some(name) => {
66                    resolved = merge_styles(resolved, &rule.style);
67                }
68                _ => {}
69            }
70        }
71
72        resolved
73    }
74}
75
76/// Merges styles for inheritance: parent provides defaults, child overrides.
77pub fn inherit_style(parent: &Style, child: &Style) -> Style {
78    Style {
79        fg: child.fg.or(parent.fg),
80        bg: child.bg.or(parent.bg),
81        bold: child.bold || parent.bold,
82        italic: child.italic || parent.italic,
83        underline: child.underline || parent.underline,
84        width: if child.width != Length::Auto { child.width } else { parent.width },
85        height: if child.height != Length::Auto { child.height } else { parent.height },
86        padding: if child.padding != crate::geom::Insets::ZERO { child.padding } else { parent.padding },
87        margin: if child.margin != crate::geom::Insets::ZERO { child.margin } else { parent.margin },
88        layout: if child.layout != Layout::None { child.layout } else { parent.layout },
89        gap: if child.gap != 0 { child.gap } else { parent.gap },
90        flex_grow: if child.flex_grow != 0 { child.flex_grow } else { parent.flex_grow },
91        flex_shrink: child.flex_shrink && parent.flex_shrink,
92        border: if child.border != Border::None { child.border } else { parent.border },
93    }
94}
95
96/// Merges two styles: `override_` values win over `base` where set.
97pub fn merge_styles(base: Style, override_: &Style) -> Style {
98    Style {
99        fg: override_.fg.or(base.fg),
100        bg: override_.bg.or(base.bg),
101        bold: override_.bold || base.bold,
102        italic: override_.italic || base.italic,
103        underline: override_.underline || base.underline,
104        width: if override_.width != Length::Auto { override_.width } else { base.width },
105        height: if override_.height != Length::Auto { override_.height } else { base.height },
106        padding: if override_.padding != crate::geom::Insets::ZERO { override_.padding } else { base.padding },
107        margin: if override_.margin != crate::geom::Insets::ZERO { override_.margin } else { base.margin },
108        layout: if override_.layout != Layout::None { override_.layout } else { base.layout },
109        gap: if override_.gap != 0 { override_.gap } else { base.gap },
110        flex_grow: if override_.flex_grow != 0 { override_.flex_grow } else { base.flex_grow },
111        flex_shrink: override_.flex_shrink && base.flex_shrink,
112        border: if override_.border != Border::None { override_.border } else { base.border },
113        ..base
114    }
115}
116
117struct Parser<'a> {
118    chars: std::iter::Peekable<std::str::Chars<'a>>,
119    pos: usize,
120}
121
122impl<'a> Parser<'a> {
123    fn new(input: &'a str) -> Self {
124        Self {
125            chars: input.chars().peekable(),
126            pos: 0,
127        }
128    }
129
130    fn parse(&mut self) -> Result<StyleSheet, String> {
131        let mut rules = Vec::new();
132        self.skip_whitespace_and_comments();
133        while self.chars.peek().is_some() {
134            rules.push(self.parse_rule()?);
135            self.skip_whitespace_and_comments();
136        }
137        Ok(StyleSheet { rules })
138    }
139
140    fn parse_rule(&mut self) -> Result<StyleRule, String> {
141        let selector = self.parse_selector()?;
142        self.skip_whitespace();
143        self.expect('{')?;
144        let style = self.parse_declarations()?;
145        self.expect('}')?;
146        Ok(StyleRule { selector, style })
147    }
148
149    fn parse_selector(&mut self) -> Result<Selector, String> {
150        let next = self.peek_char().ok_or("expected selector")?;
151        match next {
152            '.' => {
153                self.advance();
154                let name = self.parse_ident()?;
155                Ok(Selector::Class(name))
156            }
157            '#' => {
158                self.advance();
159                let name = self.parse_ident()?;
160                Ok(Selector::Id(name))
161            }
162            c if c.is_alphabetic() || c == '_' || c == '-' => {
163                let name = self.parse_ident()?;
164                Ok(Selector::Type(name))
165            }
166            _ => Err(format!("unexpected char '{}' in selector", next)),
167        }
168    }
169
170    fn parse_declarations(&mut self) -> Result<Style, String> {
171        let mut style = Style::default();
172        loop {
173            self.skip_whitespace();
174            if self.peek_char() == Some('}') || self.peek_char().is_none() {
175                break;
176            }
177            let prop = self.parse_ident()?;
178            self.skip_whitespace();
179            self.expect(':')?;
180            self.skip_whitespace();
181            let value = self.parse_value(&prop)?;
182            self.apply_property(&mut style, &prop, &value);
183            self.skip_whitespace();
184            if self.peek_char() == Some(';') {
185                self.advance();
186            }
187        }
188        Ok(style)
189    }
190
191    fn parse_value(&mut self, _prop: &str) -> Result<String, String> {
192        let mut val = String::new();
193        while let Some(&c) = self.chars.peek() {
194            if c == ';' || c == '}' || c == '\n' {
195                break;
196            }
197            val.push(c);
198            self.advance();
199        }
200        Ok(val.trim().to_string())
201    }
202
203    fn apply_property(&self, style: &mut Style, prop: &str, value: &str) {
204        match prop {
205            "fg" | "color" => {
206                if let Some(c) = parse_color(value) {
207                    style.fg = Some(c);
208                }
209            }
210            "bg" | "background" => {
211                if let Some(c) = parse_color(value) {
212                    style.bg = Some(c);
213                }
214            }
215            "bold" => style.bold = value == "true",
216            "italic" => style.italic = value == "true",
217            "underline" => style.underline = value == "true",
218            "padding" => {
219                if let Ok(n) = value.parse::<u16>() {
220                    style.padding = crate::geom::Insets::all(n);
221                }
222            }
223            "margin" => {
224                if let Ok(n) = value.parse::<u16>() {
225                    style.margin = crate::geom::Insets::all(n);
226                }
227            }
228            "gap" => {
229                if let Ok(n) = value.parse::<u16>() {
230                    style.gap = n;
231                }
232            }
233            "width" => style.width = parse_length(value),
234            "height" => style.height = parse_length(value),
235            "layout" => {
236                style.layout = match value {
237                    "vertical" | "column" => Layout::Vertical,
238                    "horizontal" | "row" => Layout::Horizontal,
239                    _ => Layout::None,
240                }
241            }
242            "border" => {
243                style.border = match value {
244                    "plain" => Border::Plain,
245                    "rounded" => Border::Rounded,
246                    "double" => Border::Double,
247                    _ => Border::None,
248                }
249            }
250            _ => {} // 忽略未知属性
251        }
252    }
253
254    fn parse_ident(&mut self) -> Result<String, String> {
255        let mut ident = String::new();
256        while let Some(&c) = self.chars.peek() {
257            if c.is_alphanumeric() || c == '_' || c == '-' {
258                ident.push(c);
259                self.advance();
260            } else {
261                break;
262            }
263        }
264        if ident.is_empty() {
265            Err("expected identifier".into())
266        } else {
267            Ok(ident)
268        }
269    }
270
271    fn skip_whitespace(&mut self) {
272        while let Some(&c) = self.chars.peek() {
273            if c.is_whitespace() {
274                self.advance();
275            } else {
276                break;
277            }
278        }
279    }
280
281    fn skip_whitespace_and_comments(&mut self) {
282        loop {
283            self.skip_whitespace();
284            // Skip // line comments
285            if self.peek_char() == Some('/') {
286                // Peek next
287                let mut iter = self.chars.clone();
288                iter.next();
289                if iter.next() == Some('/') {
290                    // Skip comment until newline
291                    while let Some(&c) = self.chars.peek() {
292                        self.advance();
293                        if c == '\n' {
294                            break;
295                        }
296                    }
297                    continue;
298                }
299            }
300            break;
301        }
302    }
303
304    fn peek_char(&mut self) -> Option<char> {
305        self.chars.peek().copied()
306    }
307
308    fn advance(&mut self) -> Option<char> {
309        self.pos += 1;
310        self.chars.next()
311    }
312
313    fn expect(&mut self, expected: char) -> Result<(), String> {
314        match self.chars.peek() {
315            Some(&c) if c == expected => {
316                self.advance();
317                Ok(())
318            }
319            Some(&c) => Err(format!("expected '{}', found '{}'", expected, c)),
320            None => Err(format!("expected '{}', found EOF", expected)),
321        }
322    }
323}
324
325fn parse_color(s: &str) -> Option<Color> {
326    match s {
327        "black" => Some(Color::Black),
328        "red" => Some(Color::Red),
329        "green" => Some(Color::Green),
330        "yellow" => Some(Color::Yellow),
331        "blue" => Some(Color::Blue),
332        "magenta" => Some(Color::Magenta),
333        "cyan" => Some(Color::Cyan),
334        "white" => Some(Color::White),
335        "gray" | "grey" => Some(Color::Gray),
336        _ => None,
337    }
338}
339
340fn parse_length(s: &str) -> Length {
341    if s == "auto" {
342        return Length::Auto;
343    }
344    if let Some(pct) = s.strip_suffix('%') {
345        if let Ok(n) = pct.parse::<u16>() {
346            return Length::Percent(n);
347        }
348    }
349    if let Some(frac) = s.strip_suffix("fr") {
350        if let Ok(n) = frac.parse::<u16>() {
351            return Length::Fraction(n);
352        }
353    }
354    if let Ok(n) = s.parse::<u16>() {
355        return Length::Fixed(n);
356    }
357    Length::Auto
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363
364    #[test]
365    fn test_parse_simple() {
366        let css = "Counter { fg: green; padding: 1; }";
367        let sheet = StyleSheet::parse(css).unwrap();
368        assert_eq!(sheet.rules.len(), 1);
369        assert_eq!(sheet.rules[0].selector, Selector::Type("Counter".into()));
370        assert_eq!(sheet.rules[0].style.fg, Some(Color::Green));
371        assert_eq!(sheet.rules[0].style.padding, crate::geom::Insets::all(1));
372    }
373
374    #[test]
375    fn test_parse_class_id() {
376        let css = ".card { border: rounded; } #header { bold: true; }";
377        let sheet = StyleSheet::parse(css).unwrap();
378        assert_eq!(sheet.rules.len(), 2);
379        assert_eq!(sheet.rules[0].selector, Selector::Class("card".into()));
380        assert_eq!(sheet.rules[1].selector, Selector::Id("header".into()));
381    }
382}