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