1use cssparser::{ParseError, Parser, Token};
4use cssparser_color::Color as ParsedColor;
5
6use crate::css::types::{
7 BorderStyle, Declaration, DockEdge, HatchStyle, LayoutDirection, Overflow, Sides, TcssColor,
8 TcssDimension, TcssDisplay, TcssValue, TextAlign, Visibility,
9};
10
11fn try_parse_variable<'i>(input: &mut Parser<'i, '_>) -> Option<String> {
17 let state = input.state();
18 match input.next() {
19 Ok(&Token::Delim('$')) => {
20 let base = match input.expect_ident_cloned() {
22 Ok(ident) => ident.to_string(),
23 Err(_) => {
24 input.reset(&state);
25 return None;
26 }
27 };
28
29 let suffix_state = input.state();
31 if let Ok(&Token::Delim('-')) = input.next() {
32 if let Ok(modifier) = input.expect_ident_cloned() {
33 if modifier == "lighten" || modifier == "darken" {
34 let dash_state = input.state();
35 if let Ok(&Token::Delim('-')) = input.next() {
36 if let Ok(&Token::Number {
37 int_value: Some(n), ..
38 }) = input.next()
39 {
40 let mut name = base;
41 name.push('-');
42 name.push_str(&modifier);
43 name.push('-');
44 name.push_str(&n.to_string());
45 return Some(name);
46 }
47 }
48 input.reset(&dash_state);
50 input.reset(&suffix_state);
52 return Some(base);
53 }
54 }
55 input.reset(&suffix_state);
57 } else {
58 input.reset(&suffix_state);
59 }
60
61 Some(base)
62 }
63 _ => {
64 input.reset(&state);
65 None
66 }
67 }
68}
69
70#[derive(Debug, Clone)]
72pub enum PropertyParseError {
73 UnknownProperty(String),
75 InvalidValue(String),
77}
78
79fn parse_color<'i>(
81 input: &mut Parser<'i, '_>,
82) -> Result<TcssColor, ParseError<'i, PropertyParseError>> {
83 let location = input.current_source_location();
84 let color = ParsedColor::parse(input).map_err(|e| {
85 location.new_custom_error(PropertyParseError::InvalidValue(format!(
86 "invalid color: {:?}",
87 e
88 )))
89 })?;
90
91 match color {
92 ParsedColor::Rgba(rgba) => {
93 if rgba.alpha >= 1.0 - f32::EPSILON {
94 Ok(TcssColor::Rgb(rgba.red, rgba.green, rgba.blue))
95 } else {
96 let alpha_u8 = (rgba.alpha * 255.0).round() as u8;
98 Ok(TcssColor::Rgba(rgba.red, rgba.green, rgba.blue, alpha_u8))
99 }
100 }
101 ParsedColor::CurrentColor => Ok(TcssColor::Reset),
102 _ => Err(location.new_custom_error(PropertyParseError::InvalidValue(
103 "unsupported color format".to_string(),
104 ))),
105 }
106}
107
108fn parse_dimension<'i>(
110 input: &mut Parser<'i, '_>,
111) -> Result<TcssDimension, ParseError<'i, PropertyParseError>> {
112 let location = input.current_source_location();
113 match input.next()? {
114 Token::Ident(name) if name.eq_ignore_ascii_case("auto") => Ok(TcssDimension::Auto),
115 Token::Number { value, .. } => Ok(TcssDimension::Length(*value)),
116 Token::Percentage { unit_value, .. } => Ok(TcssDimension::Percent(*unit_value * 100.0)),
117 Token::Dimension { value, unit, .. } if unit.eq_ignore_ascii_case("fr") => {
118 Ok(TcssDimension::Fraction(*value))
119 }
120 other => Err(
121 location.new_custom_error(PropertyParseError::InvalidValue(format!(
122 "expected dimension value, got {:?}",
123 other
124 ))),
125 ),
126 }
127}
128
129fn parse_cells<'i>(input: &mut Parser<'i, '_>) -> Result<f32, ParseError<'i, PropertyParseError>> {
131 let location = input.current_source_location();
132 match input.next()? {
133 Token::Number { value, .. } => Ok(*value),
134 other => Err(
135 location.new_custom_error(PropertyParseError::InvalidValue(format!(
136 "expected number, got {:?}",
137 other
138 ))),
139 ),
140 }
141}
142
143pub fn parse_declaration_block<'i>(
146 input: &mut Parser<'i, '_>,
147) -> Result<Vec<Declaration>, ParseError<'i, PropertyParseError>> {
148 let mut declarations = Vec::new();
149
150 loop {
151 input.skip_whitespace();
152 if input.is_exhausted() {
153 break;
154 }
155
156 let location = input.current_source_location();
158 let property_name = match input.next() {
159 Ok(Token::Ident(name)) => name.to_string(),
160 Ok(_) | Err(_) => break,
161 };
162
163 input.skip_whitespace();
164
165 match input.next() {
167 Ok(Token::Colon) => {}
168 _ => {
169 let _ = input.parse_until_after(cssparser::Delimiter::Semicolon, |_| {
171 Ok::<(), ParseError<'i, PropertyParseError>>(())
172 });
173 continue;
174 }
175 }
176
177 input.skip_whitespace();
178
179 let result = parse_property_value(input, &property_name, location);
181
182 match result {
183 Ok(Some(value)) => {
184 declarations.push(Declaration {
185 property: property_name,
186 value,
187 });
188 }
189 Ok(None) | Err(_) => {
190 let _ = input.parse_until_after(cssparser::Delimiter::Semicolon, |_| {
193 Ok::<(), ParseError<'i, PropertyParseError>>(())
194 });
195 continue;
196 }
197 }
198
199 input.skip_whitespace();
201 let state = input.state();
202 match input.next() {
203 Ok(Token::Semicolon) => {}
204 Ok(_) => {
205 input.reset(&state);
206 }
207 Err(_) => break,
208 }
209 }
210
211 Ok(declarations)
212}
213
214fn parse_property_value<'i>(
215 input: &mut Parser<'i, '_>,
216 property_name: &str,
217 location: cssparser::SourceLocation,
218) -> Result<Option<TcssValue>, ParseError<'i, PropertyParseError>> {
219 match property_name {
220 "color" | "background" => {
221 if let Some(var_name) = try_parse_variable(input) {
222 Ok(Some(TcssValue::Variable(var_name)))
223 } else {
224 Ok(Some(TcssValue::Color(parse_color(input)?)))
225 }
226 }
227 "border" => {
228 let name = input.expect_ident_cloned().map_err(|e| {
229 location.new_custom_error(PropertyParseError::InvalidValue(format!("{:?}", e)))
230 })?;
231 let style = match name.as_ref() {
232 "none" => BorderStyle::None,
233 "solid" => BorderStyle::Solid,
234 "rounded" => BorderStyle::Rounded,
235 "heavy" => BorderStyle::Heavy,
236 "double" => BorderStyle::Double,
237 "ascii" => BorderStyle::Ascii,
238 "tall" => BorderStyle::Tall,
239 "inner" | "mcgugan" => BorderStyle::McguganBox,
240 other => {
241 return Err(location.new_custom_error(PropertyParseError::InvalidValue(
242 format!("unknown border style: {}", other),
243 )));
244 }
245 };
246 if let Some(var_name) = try_parse_variable(input) {
248 return Ok(Some(TcssValue::BorderWithVariable(style, var_name)));
249 }
250 let color = parse_color(input).ok();
252 if let Some(c) = color {
253 return Ok(Some(TcssValue::BorderWithColor(style, c)));
254 }
255 Ok(Some(TcssValue::Border(style)))
256 }
257 "border-title" => {
258 let s = input.expect_string_cloned().map_err(|e| {
259 location.new_custom_error(PropertyParseError::InvalidValue(format!("{:?}", e)))
260 })?;
261 Ok(Some(TcssValue::String(s.to_string())))
262 }
263 "width" | "height" | "min-width" | "min-height" | "max-width" | "max-height" => {
264 Ok(Some(TcssValue::Dimension(parse_dimension(input)?)))
265 }
266 "padding" | "margin" => {
267 let first = parse_cells(input)?;
269 input.skip_whitespace();
270
271 let state = input.state();
273 match input.next_including_whitespace() {
274 Ok(Token::Number { value, .. }) => {
275 let second = *value;
276 input.skip_whitespace();
277 let state2 = input.state();
278 match input.next_including_whitespace() {
279 Ok(Token::Number { value, .. }) => {
280 let third = *value;
281 input.skip_whitespace();
282 let state3 = input.state();
283 match input.next_including_whitespace() {
284 Ok(Token::Number { value, .. }) => {
285 let fourth = *value;
286 Ok(Some(TcssValue::Sides(Sides {
288 top: first,
289 right: second,
290 bottom: third,
291 left: fourth,
292 })))
293 }
294 _ => {
295 input.reset(&state3);
296 Ok(Some(TcssValue::Sides(Sides {
298 top: first,
299 right: second,
300 bottom: third,
301 left: second,
302 })))
303 }
304 }
305 }
306 _ => {
307 input.reset(&state2);
308 Ok(Some(TcssValue::Sides(Sides {
310 top: first,
311 right: second,
312 bottom: first,
313 left: second,
314 })))
315 }
316 }
317 }
318 _ => {
319 input.reset(&state);
320 Ok(Some(TcssValue::Float(first)))
322 }
323 }
324 }
325 "display" => {
326 let name = input.expect_ident_cloned().map_err(|e| {
327 location.new_custom_error(PropertyParseError::InvalidValue(format!("{:?}", e)))
328 })?;
329 let d = match name.as_ref() {
330 "flex" => TcssDisplay::Flex,
331 "grid" => TcssDisplay::Grid,
332 "block" => TcssDisplay::Block,
333 "none" => TcssDisplay::None,
334 other => {
335 return Err(location.new_custom_error(PropertyParseError::InvalidValue(
336 format!("unknown display value: {}", other),
337 )));
338 }
339 };
340 Ok(Some(TcssValue::Display(d)))
341 }
342 "visibility" => {
343 let name = input.expect_ident_cloned().map_err(|e| {
344 location.new_custom_error(PropertyParseError::InvalidValue(format!("{:?}", e)))
345 })?;
346 let v = match name.as_ref() {
347 "visible" => Visibility::Visible,
348 "hidden" => Visibility::Hidden,
349 other => {
350 return Err(location.new_custom_error(PropertyParseError::InvalidValue(
351 format!("unknown visibility value: {}", other),
352 )));
353 }
354 };
355 Ok(Some(TcssValue::Visibility(v)))
356 }
357 "opacity" | "flex-grow" => {
358 let v = input.expect_number().map_err(|e| {
359 location.new_custom_error(PropertyParseError::InvalidValue(format!("{:?}", e)))
360 })?;
361 Ok(Some(TcssValue::Float(v)))
362 }
363 "text-align" => {
364 let name = input.expect_ident_cloned().map_err(|e| {
365 location.new_custom_error(PropertyParseError::InvalidValue(format!("{:?}", e)))
366 })?;
367 let a = match name.as_ref() {
368 "left" => TextAlign::Left,
369 "center" => TextAlign::Center,
370 "right" => TextAlign::Right,
371 other => {
372 return Err(location.new_custom_error(PropertyParseError::InvalidValue(
373 format!("unknown text-align value: {}", other),
374 )));
375 }
376 };
377 Ok(Some(TcssValue::TextAlign(a)))
378 }
379 "overflow" => {
380 let name = input.expect_ident_cloned().map_err(|e| {
381 location.new_custom_error(PropertyParseError::InvalidValue(format!("{:?}", e)))
382 })?;
383 let o = match name.as_ref() {
384 "visible" => Overflow::Visible,
385 "hidden" => Overflow::Hidden,
386 "scroll" => Overflow::Scroll,
387 "auto" => Overflow::Auto,
388 other => {
389 return Err(location.new_custom_error(PropertyParseError::InvalidValue(
390 format!("unknown overflow value: {}", other),
391 )));
392 }
393 };
394 Ok(Some(TcssValue::Overflow(o)))
395 }
396 "scrollbar-gutter" => {
397 let name = input.expect_ident_cloned().map_err(|e| {
398 location.new_custom_error(PropertyParseError::InvalidValue(format!("{:?}", e)))
399 })?;
400 let b = match name.as_ref() {
401 "stable" => true,
402 "auto" => false,
403 other => {
404 return Err(location.new_custom_error(PropertyParseError::InvalidValue(
405 format!("unknown scrollbar-gutter value: {}", other),
406 )));
407 }
408 };
409 Ok(Some(TcssValue::Bool(b)))
410 }
411 "dock" => {
412 let name = input.expect_ident_cloned().map_err(|e| {
413 location.new_custom_error(PropertyParseError::InvalidValue(format!("{:?}", e)))
414 })?;
415 let d = match name.as_ref() {
416 "top" => DockEdge::Top,
417 "bottom" => DockEdge::Bottom,
418 "left" => DockEdge::Left,
419 "right" => DockEdge::Right,
420 other => {
421 return Err(location.new_custom_error(PropertyParseError::InvalidValue(
422 format!("unknown dock value: {}", other),
423 )));
424 }
425 };
426 Ok(Some(TcssValue::DockEdge(d)))
427 }
428 "grid-template-columns" | "grid-template-rows" => {
429 let mut dims = Vec::new();
431 loop {
432 input.skip_whitespace();
433 let state = input.state();
434 match parse_dimension(input) {
435 Ok(d) => dims.push(d),
436 Err(_) => {
437 input.reset(&state);
438 break;
439 }
440 }
441 }
442 if dims.is_empty() {
443 Ok(None)
444 } else {
445 Ok(Some(TcssValue::Dimensions(dims)))
446 }
447 }
448 "layout-direction" => {
449 let name = input.expect_ident_cloned().map_err(|e| {
450 location.new_custom_error(PropertyParseError::InvalidValue(format!("{:?}", e)))
451 })?;
452 let d = match name.as_ref() {
453 "vertical" => LayoutDirection::Vertical,
454 "horizontal" => LayoutDirection::Horizontal,
455 other => {
456 return Err(location.new_custom_error(PropertyParseError::InvalidValue(
457 format!("unknown layout-direction value: {}", other),
458 )));
459 }
460 };
461 Ok(Some(TcssValue::LayoutDirection(d)))
462 }
463 "hatch" => {
464 let name = input.expect_ident_cloned().map_err(|e| {
465 location.new_custom_error(PropertyParseError::InvalidValue(format!("{:?}", e)))
466 })?;
467 let style = match name.as_ref() {
468 "cross" => HatchStyle::Cross,
469 "horizontal" => HatchStyle::Horizontal,
470 "vertical" => HatchStyle::Vertical,
471 "left" => HatchStyle::Left,
472 "right" => HatchStyle::Right,
473 other => {
474 return Err(location.new_custom_error(PropertyParseError::InvalidValue(
475 format!("unknown hatch style: {}", other),
476 )));
477 }
478 };
479 Ok(Some(TcssValue::Hatch(style)))
480 }
481 "keyline" => {
482 if let Some(var_name) = try_parse_variable(input) {
483 Ok(Some(TcssValue::KeylineVariable(var_name)))
484 } else {
485 Ok(Some(TcssValue::Keyline(parse_color(input)?)))
486 }
487 }
488 _other => Ok(None),
490 }
491}
492
493#[cfg(test)]
494mod tests {
495 use super::*;
496
497 fn parse_decl(css: &str) -> Declaration {
498 let input_str = format!("{};", css);
499 let mut input = cssparser::ParserInput::new(&input_str);
500 let mut parser = cssparser::Parser::new(&mut input);
501 let decls = parse_declaration_block(&mut parser).expect("parse failed");
502 assert!(!decls.is_empty(), "no declaration parsed from: {}", css);
503 decls.into_iter().next().unwrap()
504 }
505
506 fn parse_decl_value(css: &str) -> TcssValue {
507 parse_decl(css).value
508 }
509
510 #[test]
511 fn parse_color_named() {
512 let val = parse_decl_value("color: red");
513 assert!(matches!(val, TcssValue::Color(TcssColor::Rgb(255, 0, 0))));
515 }
516
517 #[test]
518 fn parse_color_hex_6() {
519 let val = parse_decl_value("color: #ff0000");
520 assert!(matches!(val, TcssValue::Color(TcssColor::Rgb(255, 0, 0))));
521 }
522
523 #[test]
524 fn parse_color_rgb_function() {
525 let val = parse_decl_value("color: rgb(255, 0, 0)");
526 assert!(matches!(val, TcssValue::Color(TcssColor::Rgb(255, 0, 0))));
527 }
528
529 #[test]
530 fn parse_color_rgba_function() {
531 let val = parse_decl_value("color: rgba(255, 0, 0, 0.5)");
533 assert!(matches!(
534 val,
535 TcssValue::Color(TcssColor::Rgba(255, 0, 0, _))
536 ));
537 }
538
539 #[test]
540 fn parse_color_hex_3() {
541 let val = parse_decl_value("color: #f00");
542 assert!(matches!(val, TcssValue::Color(TcssColor::Rgb(255, 0, 0))));
543 }
544
545 #[test]
546 fn parse_width_number() {
547 let val = parse_decl_value("width: 20");
548 assert_eq!(val, TcssValue::Dimension(TcssDimension::Length(20.0)));
549 }
550
551 #[test]
552 fn parse_width_percent() {
553 let val = parse_decl_value("width: 50%");
554 assert_eq!(val, TcssValue::Dimension(TcssDimension::Percent(50.0)));
555 }
556
557 #[test]
558 fn parse_width_fraction() {
559 let val = parse_decl_value("width: 1fr");
560 assert_eq!(val, TcssValue::Dimension(TcssDimension::Fraction(1.0)));
561 }
562
563 #[test]
564 fn parse_width_auto() {
565 let val = parse_decl_value("width: auto");
566 assert_eq!(val, TcssValue::Dimension(TcssDimension::Auto));
567 }
568
569 #[test]
570 fn parse_border_solid() {
571 let val = parse_decl_value("border: solid");
572 assert_eq!(val, TcssValue::Border(BorderStyle::Solid));
573 }
574
575 #[test]
576 fn parse_border_rounded() {
577 let val = parse_decl_value("border: rounded");
578 assert_eq!(val, TcssValue::Border(BorderStyle::Rounded));
579 }
580
581 #[test]
582 fn parse_display_none() {
583 let val = parse_decl_value("display: none");
584 assert_eq!(val, TcssValue::Display(TcssDisplay::None));
585 }
586
587 #[test]
588 fn parse_opacity() {
589 let val = parse_decl_value("opacity: 0.5");
590 assert_eq!(val, TcssValue::Float(0.5));
591 }
592
593 #[test]
594 fn parse_dock_top() {
595 let val = parse_decl_value("dock: top");
596 assert_eq!(val, TcssValue::DockEdge(DockEdge::Top));
597 }
598
599 #[test]
602 fn parse_color_variable_primary() {
603 let val = parse_decl_value("color: $primary");
604 assert_eq!(val, TcssValue::Variable("primary".to_string()));
605 }
606
607 #[test]
608 fn parse_background_variable() {
609 let val = parse_decl_value("background: $surface");
610 assert_eq!(val, TcssValue::Variable("surface".to_string()));
611 }
612
613 #[test]
614 fn parse_variable_lighten_suffix() {
615 let val = parse_decl_value("color: $primary-lighten-2");
616 assert_eq!(val, TcssValue::Variable("primary-lighten-2".to_string()));
617 }
618
619 #[test]
620 fn parse_variable_darken_suffix() {
621 let val = parse_decl_value("color: $accent-darken-1");
622 assert_eq!(val, TcssValue::Variable("accent-darken-1".to_string()));
623 }
624
625 #[test]
626 fn parse_variable_darken_3() {
627 let val = parse_decl_value("background: $error-darken-3");
628 assert_eq!(val, TcssValue::Variable("error-darken-3".to_string()));
629 }
630
631 #[test]
632 fn parse_regular_color_still_works_after_variable_support() {
633 let val = parse_decl_value("color: #ff0000");
635 assert!(matches!(val, TcssValue::Color(TcssColor::Rgb(255, 0, 0))));
636
637 let val2 = parse_decl_value("color: red");
638 assert!(matches!(val2, TcssValue::Color(TcssColor::Rgb(255, 0, 0))));
639
640 let val3 = parse_decl_value("background: rgb(0, 255, 0)");
641 assert!(matches!(val3, TcssValue::Color(TcssColor::Rgb(0, 255, 0))));
642 }
643
644 #[test]
647 fn parse_border_tall_variable() {
648 let val = parse_decl_value("border: tall $primary");
649 assert_eq!(
650 val,
651 TcssValue::BorderWithVariable(BorderStyle::Tall, "primary".to_string())
652 );
653 }
654
655 #[test]
656 fn parse_border_rounded_variable_with_suffix() {
657 let val = parse_decl_value("border: rounded $accent-lighten-2");
658 assert_eq!(
659 val,
660 TcssValue::BorderWithVariable(BorderStyle::Rounded, "accent-lighten-2".to_string())
661 );
662 }
663
664 #[test]
665 fn parse_border_solid_hex_still_works() {
666 let val = parse_decl_value("border: solid #ff0000");
667 match val {
668 TcssValue::BorderWithColor(BorderStyle::Solid, TcssColor::Rgb(255, 0, 0)) => {}
669 other => panic!(
670 "expected BorderWithColor(Solid, Rgb(255,0,0)), got {:?}",
671 other
672 ),
673 }
674 }
675
676 #[test]
677 fn parse_border_heavy_no_color() {
678 let val = parse_decl_value("border: heavy");
679 assert_eq!(val, TcssValue::Border(BorderStyle::Heavy));
680 }
681
682 #[test]
683 fn parse_unknown_variable_produces_variable_variant() {
684 let val = parse_decl_value("color: $nonexistent");
686 assert_eq!(val, TcssValue::Variable("nonexistent".to_string()));
687 }
688
689 #[test]
692 fn parse_hatch_cross() {
693 let val = parse_decl_value("hatch: cross");
694 assert_eq!(val, TcssValue::Hatch(HatchStyle::Cross));
695 }
696
697 #[test]
698 fn parse_hatch_horizontal() {
699 let val = parse_decl_value("hatch: horizontal");
700 assert_eq!(val, TcssValue::Hatch(HatchStyle::Horizontal));
701 }
702
703 #[test]
704 fn parse_hatch_vertical() {
705 let val = parse_decl_value("hatch: vertical");
706 assert_eq!(val, TcssValue::Hatch(HatchStyle::Vertical));
707 }
708
709 #[test]
710 fn parse_hatch_left() {
711 let val = parse_decl_value("hatch: left");
712 assert_eq!(val, TcssValue::Hatch(HatchStyle::Left));
713 }
714
715 #[test]
716 fn parse_hatch_right() {
717 let val = parse_decl_value("hatch: right");
718 assert_eq!(val, TcssValue::Hatch(HatchStyle::Right));
719 }
720
721 #[test]
724 fn parse_keyline_color() {
725 let val = parse_decl_value("keyline: #ff0000");
726 assert_eq!(val, TcssValue::Keyline(TcssColor::Rgb(255, 0, 0)));
727 }
728
729 #[test]
730 fn parse_keyline_variable() {
731 let val = parse_decl_value("keyline: $primary");
732 assert_eq!(val, TcssValue::KeylineVariable("primary".to_string()));
733 }
734}