1use std::collections::HashMap;
89
90use cssparser::{
91 AtRuleParser, CowRcStr, DeclarationParser, ParseError, Parser, ParserInput, ParserState,
92 QualifiedRuleParser, RuleBodyItemParser, RuleBodyParser, Token,
93};
94
95use super::attributes::StyleAttributes;
96use super::color::ColorDef;
97use super::definition::StyleDefinition;
98use super::error::StylesheetError;
99use super::parser::{build_variants, ThemeVariants};
100
101pub fn parse_css(css: &str) -> Result<ThemeVariants, StylesheetError> {
103 let mut input = ParserInput::new(css);
104 let mut parser = Parser::new(&mut input);
105
106 let mut css_parser = StyleSheetParser {
107 definitions: HashMap::new(),
108 current_mode: None,
109 };
110
111 let rule_list_parser = cssparser::StyleSheetParser::new(&mut parser, &mut css_parser);
112
113 for result in rule_list_parser {
114 if let Err(e) = result {
115 return Err(StylesheetError::Parse {
117 path: None,
118 message: format!("CSS Parse Error: {:?}", e),
119 });
120 }
121 }
122
123 build_variants(&css_parser.definitions)
124}
125
126struct StyleSheetParser {
127 definitions: HashMap<String, StyleDefinition>,
128 current_mode: Option<Mode>,
129}
130
131#[derive(Clone, Copy, PartialEq, Eq)]
132enum Mode {
133 Light,
134 Dark,
135}
136
137impl<'i> QualifiedRuleParser<'i> for StyleSheetParser {
138 type Prelude = Vec<String>;
139 type QualifiedRule = ();
140 type Error = ();
141
142 fn parse_prelude<'t>(
143 &mut self,
144 input: &mut Parser<'i, 't>,
145 ) -> Result<Self::Prelude, ParseError<'i, Self::Error>> {
146 let mut names = Vec::new();
147
148 while let Ok(token) = input.next() {
149 match token {
150 Token::Delim('.') => {
151 let name = input.expect_ident()?;
152 names.push(name.as_ref().to_string());
153 }
154 Token::Comma | Token::WhiteSpace(_) => continue,
155 _ => {
156 }
158 }
159 }
160
161 if names.is_empty() {
162 return Err(input.new_custom_error::<(), ()>(()));
163 }
164 Ok(names)
165 }
166
167 fn parse_block<'t>(
168 &mut self,
169 prelude: Self::Prelude,
170 _start: &ParserState,
171 input: &mut Parser<'i, 't>,
172 ) -> Result<Self::QualifiedRule, ParseError<'i, Self::Error>> {
173 let mut decl_parser = StyleDeclarationParser;
174 let rule_parser = RuleBodyParser::new(input, &mut decl_parser);
175
176 let mut attributes = StyleAttributes::new();
177
178 for (_prop, val) in rule_parser.flatten() {
179 if let Some(c) = val.fg {
180 attributes.fg = Some(c);
181 }
182 if let Some(c) = val.bg {
183 attributes.bg = Some(c);
184 }
185 if let Some(b) = val.bold {
186 attributes.bold = Some(b);
187 }
188 if let Some(v) = val.dim {
189 attributes.dim = Some(v);
190 }
191 if let Some(v) = val.italic {
192 attributes.italic = Some(v);
193 }
194 if let Some(v) = val.underline {
195 attributes.underline = Some(v);
196 }
197 if let Some(v) = val.blink {
198 attributes.blink = Some(v);
199 }
200 if let Some(v) = val.reverse {
201 attributes.reverse = Some(v);
202 }
203 if let Some(v) = val.hidden {
204 attributes.hidden = Some(v);
205 }
206 if let Some(v) = val.strikethrough {
207 attributes.strikethrough = Some(v);
208 }
209 }
210
211 for name in prelude {
212 let def = self
213 .definitions
214 .entry(name)
215 .or_insert(StyleDefinition::Attributes {
216 base: StyleAttributes::new(),
217 light: None,
218 dark: None,
219 });
220
221 if let StyleDefinition::Attributes {
222 ref mut base,
223 ref mut light,
224 ref mut dark,
225 } = def
226 {
227 match self.current_mode {
228 None => *base = base.merge(&attributes),
229 Some(Mode::Light) => {
230 let l = light.get_or_insert(StyleAttributes::new());
231 *l = l.merge(&attributes);
232 }
233 Some(Mode::Dark) => {
234 let d = dark.get_or_insert(StyleAttributes::new());
235 *d = d.merge(&attributes);
236 }
237 }
238 }
239 }
240 Ok(())
241 }
242}
243
244impl<'i> AtRuleParser<'i> for StyleSheetParser {
245 type Prelude = Mode;
246 type AtRule = ();
247 type Error = ();
248
249 fn parse_prelude<'t>(
250 &mut self,
251 name: CowRcStr<'i>,
252 input: &mut Parser<'i, 't>,
253 ) -> Result<Self::Prelude, ParseError<'i, Self::Error>> {
254 if name.as_ref() == "media" {
255 let mut found_mode: Option<Mode> = None;
257
258 loop {
259 match input.next() {
260 Ok(Token::ParenthesisBlock) => {
261 let nested_res = input.parse_nested_block(|input| {
263 input.expect_ident_matching("prefers-color-scheme")?;
264 input.expect_colon()?;
265 let val = input.expect_ident()?;
266 match val.as_ref() {
267 "dark" => Ok(Mode::Dark),
268 "light" => Ok(Mode::Light),
269 _ => Err(input.new_custom_error::<(), ()>(())),
270 }
271 });
272 if let Ok(m) = nested_res {
273 found_mode = Some(m);
274 }
275 }
276 Ok(Token::WhiteSpace(_)) | Ok(Token::Comment(_)) => continue,
277 Err(_) => break, Ok(_) => {
279 }
281 }
282 }
283
284 if let Some(m) = found_mode {
285 return Ok(m);
286 }
287
288 Err(input.new_custom_error::<(), ()>(()))
289 } else {
290 Err(input.new_custom_error::<(), ()>(()))
291 }
292 }
293
294 fn parse_block<'t>(
295 &mut self,
296 mode: Self::Prelude,
297 _start: &ParserState,
298 input: &mut Parser<'i, 't>,
299 ) -> Result<Self::AtRule, ParseError<'i, Self::Error>> {
300 let old_mode = self.current_mode;
301 self.current_mode = Some(mode);
302
303 let list_parser = cssparser::StyleSheetParser::new(input, self);
304 for _ in list_parser {}
305
306 self.current_mode = old_mode;
307 Ok(())
308 }
309}
310
311struct StyleDeclarationParser;
312
313impl<'i> DeclarationParser<'i> for StyleDeclarationParser {
314 type Declaration = (String, StyleAttributes);
315 type Error = ();
316
317 fn parse_value<'t>(
318 &mut self,
319 name: CowRcStr<'i>,
320 input: &mut Parser<'i, 't>,
321 ) -> Result<Self::Declaration, ParseError<'i, Self::Error>> {
322 let mut attrs = StyleAttributes::new();
323 match name.as_ref() {
324 "fg" | "color" => {
325 attrs.fg = Some(parse_color(input)?);
326 }
327 "bg" | "background" | "background-color" => {
328 attrs.bg = Some(parse_color(input)?);
329 }
330 "bold" => {
331 if parse_bool_or_flag(input)? {
332 attrs.bold = Some(true);
333 }
334 }
335 "dim" => {
336 if parse_bool_or_flag(input)? {
337 attrs.dim = Some(true);
338 }
339 }
340 "italic" => {
341 if parse_bool_or_flag(input)? {
342 attrs.italic = Some(true);
343 }
344 }
345 "underline" => {
346 if parse_bool_or_flag(input)? {
347 attrs.underline = Some(true);
348 }
349 }
350 "blink" => {
351 if parse_bool_or_flag(input)? {
352 attrs.blink = Some(true);
353 }
354 }
355 "reverse" => {
356 if parse_bool_or_flag(input)? {
357 attrs.reverse = Some(true);
358 }
359 }
360 "hidden" => {
361 if parse_bool_or_flag(input)? {
362 attrs.hidden = Some(true);
363 }
364 }
365 "strikethrough" => {
366 if parse_bool_or_flag(input)? {
367 attrs.strikethrough = Some(true);
368 }
369 }
370
371 "font-weight" => {
372 let val = input.expect_ident()?;
373 if val.as_ref() == "bold" {
374 attrs.bold = Some(true);
375 }
376 }
377 "font-style" => {
378 let val = input.expect_ident()?;
379 if val.as_ref() == "italic" {
380 attrs.italic = Some(true);
381 }
382 }
383 "text-decoration" => {
384 let val = input.expect_ident()?;
385 match val.as_ref() {
386 "underline" => attrs.underline = Some(true),
387 "line-through" => attrs.strikethrough = Some(true),
388 _ => {}
389 }
390 }
391 "visibility" => {
392 let val = input.expect_ident()?;
393 if val.as_ref() == "hidden" {
394 attrs.hidden = Some(true);
395 }
396 }
397
398 _ => return Err(input.new_custom_error::<(), ()>(())),
399 }
400 Ok((name.as_ref().to_string(), attrs))
401 }
402}
403
404impl<'i> AtRuleParser<'i> for StyleDeclarationParser {
405 type Prelude = ();
406 type AtRule = (String, StyleAttributes);
407 type Error = ();
408}
409
410impl<'i> QualifiedRuleParser<'i> for StyleDeclarationParser {
411 type Prelude = ();
412 type QualifiedRule = (String, StyleAttributes);
413 type Error = ();
414}
415
416impl<'i> RuleBodyItemParser<'i, (String, StyleAttributes), ()> for StyleDeclarationParser {
417 fn parse_declarations(&self) -> bool {
418 true
419 }
420 fn parse_qualified(&self) -> bool {
421 false
422 }
423}
424
425fn parse_color<'i, 't>(input: &mut Parser<'i, 't>) -> Result<ColorDef, ParseError<'i, ()>> {
426 let token = match input.next() {
427 Ok(t) => t,
428 Err(_) => return Err(input.new_custom_error::<(), ()>(())),
429 };
430
431 match token {
432 Token::Ident(name) => {
433 ColorDef::parse_string(name.as_ref()).map_err(|_| input.new_custom_error::<(), ()>(()))
434 }
435 Token::Hash(val) | Token::IDHash(val) => ColorDef::parse_string(&format!("#{}", val))
436 .map_err(|_| input.new_custom_error::<(), ()>(())),
437 _ => Err(input.new_custom_error::<(), ()>(())),
438 }
439}
440
441fn parse_bool_or_flag<'i, 't>(input: &mut Parser<'i, 't>) -> Result<bool, ParseError<'i, ()>> {
442 match input.expect_ident() {
443 Ok(val) => Ok(val.as_ref() == "true"),
444 Err(_) => Err(input.new_custom_error::<(), ()>(())),
445 }
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451 use crate::{ColorMode, StyleValue};
452
453 #[test]
454 fn test_parse_simple() {
455 let css = ".error { color: red; font-weight: bold; }";
456 let variants = parse_css(css).unwrap();
457 let base = variants.base();
458
459 assert!(base.contains_key("error"));
461
462 let style = base.get("error").unwrap().clone().force_styling(true);
463 let styled = style.apply_to("text").to_string();
464 assert!(styled.contains("\x1b[31m"));
466 assert!(styled.contains("\x1b[1m"));
467 }
468
469 #[test]
470 fn test_parse_adaptive() {
471 let css =
472 ".text { color: red; } @media (prefers-color-scheme: dark) { .text { color: white; } }";
473 let variants = parse_css(css).unwrap();
474
475 let light = variants.resolve(Some(ColorMode::Light));
476 let dark = variants.resolve(Some(ColorMode::Dark));
477
478 if let StyleValue::Concrete(s) = light.get("text").unwrap() {
480 let out = s.clone().force_styling(true).apply_to("x").to_string();
481 assert!(out.contains("\x1b[31m")); } else {
483 panic!("Expected Concrete style for light mode");
484 }
485
486 if let StyleValue::Concrete(s) = dark.get("text").unwrap() {
488 let out = s.clone().force_styling(true).apply_to("x").to_string();
489 assert!(out.contains("\x1b[37m")); } else {
491 panic!("Expected Concrete style for dark mode");
492 }
493 }
494
495 #[test]
496 fn test_multiple_selectors() {
497 let css = ".a, .b { color: blue; }";
498 let variants = parse_css(css).unwrap();
499 let base = variants.base();
500 assert!(base.contains_key("a"));
501 assert!(base.contains_key("b"));
502 }
503
504 #[test]
505 fn test_all_properties() {
506 let css = r#"
507 .all-props {
508 fg: red;
509 bg: blue;
510 bold: true;
511 dim: true;
512 italic: true;
513 underline: true;
514 blink: true;
515 reverse: true;
516 hidden: true;
517 strikethrough: true;
518 }
519 "#;
520 let variants = parse_css(css).unwrap();
521 let base = variants.base();
522 assert!(base.contains_key("all-props"));
523
524 let style = base.get("all-props").unwrap().clone().force_styling(true);
529 let out = style.apply_to("text").to_string();
530
531 assert!(out.contains("\x1b[31m")); assert!(out.contains("\x1b[44m")); assert!(out.contains("\x1b[1m")); assert!(out.contains("\x1b[2m")); assert!(out.contains("\x1b[3m")); assert!(out.contains("\x1b[4m")); assert!(out.contains("\x1b[5m")); assert!(out.contains("\x1b[7m")); assert!(out.contains("\x1b[8m")); assert!(out.contains("\x1b[9m")); }
542
543 #[test]
544 fn test_css_aliases() {
545 let css = r#"
546 .aliases {
547 background-color: green;
548 font-weight: bold;
549 font-style: italic;
550 text-decoration: underline;
551 visibility: hidden;
552 }
553 "#;
554 let variants = parse_css(css).unwrap();
555 let base = variants.base();
556 let style = base.get("aliases").unwrap().clone().force_styling(true);
557 let out = style.apply_to("text").to_string();
558
559 assert!(out.contains("\x1b[42m")); assert!(out.contains("\x1b[1m")); assert!(out.contains("\x1b[3m")); assert!(out.contains("\x1b[4m")); assert!(out.contains("\x1b[8m")); }
565
566 #[test]
567 fn test_text_decoration_line_through() {
568 let css = ".strike { text-decoration: line-through; }";
569 let variants = parse_css(css).unwrap();
570 let style = variants
571 .base()
572 .get("strike")
573 .unwrap()
574 .clone()
575 .force_styling(true);
576 let out = style.apply_to("text").to_string();
577 assert!(out.contains("\x1b[9m"));
578 }
579
580 #[test]
581 fn test_invalid_syntax_recovery() {
582 let css = r#"
584 .broken {
585 color: ;
586 unknown: prop;
587 bold: not-a-bool;
588 }
589 .valid { color: cyan; }
590 "#;
591
592 let variants = parse_css(css).unwrap();
594 assert!(variants.base().contains_key("valid"));
595 }
596
597 #[test]
598 fn test_empty_selector_error() {
599 let css = ". { color: red; }";
601 let res = parse_css(css);
602 assert!(res.is_err());
603 }
604
605 #[test]
606 fn test_no_dot_selector() {
607 let css = "body { color: red; }";
609 let res = parse_css(css);
613 assert!(res.is_err());
614 }
615
616 #[test]
617 fn test_invalid_color() {
618 let css = ".bad-color { color: not-a-color; }";
619 let variants = parse_css(css).unwrap();
621 assert!(variants.base().contains_key("bad-color"));
622 }
623
624 #[test]
625 fn test_hex_colors() {
626 let css = ".hex { color: #ff0000; bg: #00ff00; }";
627 let variants = parse_css(css).unwrap();
628 let style = variants.base().get("hex").unwrap();
629 let out = style.apply_to("x").to_string();
630 assert!(!out.is_empty());
632 }
633
634 #[test]
635 fn test_comments() {
636 let css = r#"
637 /* This is a comment */
638 .commented {
639 color: red; /* Inline comment */
640 }
641 "#;
642 let variants = parse_css(css).unwrap();
643 assert!(variants.base().contains_key("commented"));
644 }
645
646 use proptest::prelude::*;
647
648 proptest! {
649 #[test]
650 fn test_random_css_input_no_panic(s in "\\PC*") {
651 let _ = parse_css(&s);
653 }
654
655 #[test]
656 fn test_valid_structure_random_values(
657 color in "[a-zA-Z]+",
658 bool_val in "true|false",
659 prop_name in "[a-z-]+"
660 ) {
661 let css = format!(".prop {{ color: {}; bold: {}; {}: {}; }}", color, bool_val, prop_name, bool_val);
662 let _ = parse_css(&css);
663 }
664 }
665}