1use super::attributes::{parse_shorthand, StyleAttributes};
33use super::error::StylesheetError;
34
35#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum StyleDefinition {
40 Alias(String),
44
45 Attributes {
51 base: StyleAttributes,
53 light: Option<StyleAttributes>,
55 dark: Option<StyleAttributes>,
57 },
58}
59
60impl StyleDefinition {
61 pub fn parse(value: &serde_yaml::Value, style_name: &str) -> Result<Self, StylesheetError> {
67 match value {
68 serde_yaml::Value::String(s) => Self::parse_string(s, style_name),
69 serde_yaml::Value::Mapping(map) => Self::parse_mapping(map, style_name),
70 _ => Err(StylesheetError::InvalidDefinition {
71 style: style_name.to_string(),
72 message: format!("Expected string or mapping, got {:?}", value),
73 path: None,
74 }),
75 }
76 }
77
78 fn parse_string(s: &str, style_name: &str) -> Result<Self, StylesheetError> {
83 let s = s.trim();
84
85 if s.is_empty() {
87 return Err(StylesheetError::InvalidDefinition {
88 style: style_name.to_string(),
89 message: "Empty style definition".to_string(),
90 path: None,
91 });
92 }
93
94 if s.contains(' ') {
96 let attrs = parse_shorthand(s, style_name)?;
97 return Ok(StyleDefinition::Attributes {
98 base: attrs,
99 light: None,
100 dark: None,
101 });
102 }
103
104 match parse_shorthand(s, style_name) {
107 Ok(attrs) => {
108 if is_likely_alias(s) {
110 Ok(StyleDefinition::Alias(s.to_string()))
112 } else {
113 Ok(StyleDefinition::Attributes {
115 base: attrs,
116 light: None,
117 dark: None,
118 })
119 }
120 }
121 Err(_) => {
122 Ok(StyleDefinition::Alias(s.to_string()))
124 }
125 }
126 }
127
128 fn parse_mapping(map: &serde_yaml::Mapping, style_name: &str) -> Result<Self, StylesheetError> {
130 let base = StyleAttributes::parse_mapping(map, style_name)?;
132
133 let light = if let Some(light_val) = map.get(serde_yaml::Value::String("light".into())) {
135 let light_map =
136 light_val
137 .as_mapping()
138 .ok_or_else(|| StylesheetError::InvalidDefinition {
139 style: style_name.to_string(),
140 message: "'light' must be a mapping".to_string(),
141 path: None,
142 })?;
143 Some(StyleAttributes::parse_mapping(light_map, style_name)?)
144 } else {
145 None
146 };
147
148 let dark = if let Some(dark_val) = map.get(serde_yaml::Value::String("dark".into())) {
150 let dark_map =
151 dark_val
152 .as_mapping()
153 .ok_or_else(|| StylesheetError::InvalidDefinition {
154 style: style_name.to_string(),
155 message: "'dark' must be a mapping".to_string(),
156 path: None,
157 })?;
158 Some(StyleAttributes::parse_mapping(dark_map, style_name)?)
159 } else {
160 None
161 };
162
163 Ok(StyleDefinition::Attributes { base, light, dark })
164 }
165
166 pub fn is_alias(&self) -> bool {
168 matches!(self, StyleDefinition::Alias(_))
169 }
170
171 pub fn alias_target(&self) -> Option<&str> {
173 match self {
174 StyleDefinition::Alias(target) => Some(target),
175 _ => None,
176 }
177 }
178}
179
180fn is_likely_alias(s: &str) -> bool {
184 let lower = s.to_lowercase();
185
186 let attributes = [
188 "bold",
189 "dim",
190 "italic",
191 "underline",
192 "blink",
193 "reverse",
194 "hidden",
195 "strikethrough",
196 ];
197
198 if attributes.contains(&lower.as_str()) {
199 return false;
200 }
201
202 let colors = [
204 "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "gray", "grey",
205 ];
206
207 if colors.contains(&lower.as_str()) {
208 return false;
209 }
210
211 if lower.starts_with("bright_") {
213 return false;
214 }
215
216 if s.starts_with('#') {
218 return false;
219 }
220
221 true
223}
224
225#[cfg(test)]
226mod tests {
227 use super::super::color::ColorDef;
228 use super::*;
229 use console::Color;
230
231 #[test]
236 fn test_parse_alias() {
237 let value = serde_yaml::Value::String("muted".into());
238 let def = StyleDefinition::parse(&value, "test").unwrap();
239 assert!(matches!(def, StyleDefinition::Alias(s) if s == "muted"));
240 }
241
242 #[test]
243 fn test_parse_alias_with_underscore() {
244 let value = serde_yaml::Value::String("my_style".into());
245 let def = StyleDefinition::parse(&value, "test").unwrap();
246 assert!(matches!(def, StyleDefinition::Alias(s) if s == "my_style"));
247 }
248
249 #[test]
250 fn test_parse_alias_with_hyphen() {
251 let value = serde_yaml::Value::String("my-style".into());
252 let def = StyleDefinition::parse(&value, "test").unwrap();
253 assert!(matches!(def, StyleDefinition::Alias(s) if s == "my-style"));
254 }
255
256 #[test]
261 fn test_parse_shorthand_single_attribute() {
262 let value = serde_yaml::Value::String("bold".into());
263 let def = StyleDefinition::parse(&value, "test").unwrap();
264 match def {
265 StyleDefinition::Attributes { base, light, dark } => {
266 assert_eq!(base.bold, Some(true));
267 assert!(light.is_none());
268 assert!(dark.is_none());
269 }
270 _ => panic!("Expected Attributes"),
271 }
272 }
273
274 #[test]
275 fn test_parse_shorthand_single_color() {
276 let value = serde_yaml::Value::String("cyan".into());
277 let def = StyleDefinition::parse(&value, "test").unwrap();
278 match def {
279 StyleDefinition::Attributes { base, .. } => {
280 assert_eq!(base.fg, Some(ColorDef::Named(Color::Cyan)));
281 }
282 _ => panic!("Expected Attributes"),
283 }
284 }
285
286 #[test]
287 fn test_parse_shorthand_multiple() {
288 let value = serde_yaml::Value::String("yellow bold italic".into());
289 let def = StyleDefinition::parse(&value, "test").unwrap();
290 match def {
291 StyleDefinition::Attributes { base, .. } => {
292 assert_eq!(base.fg, Some(ColorDef::Named(Color::Yellow)));
293 assert_eq!(base.bold, Some(true));
294 assert_eq!(base.italic, Some(true));
295 }
296 _ => panic!("Expected Attributes"),
297 }
298 }
299
300 #[test]
305 fn test_parse_mapping_simple() {
306 let yaml = r#"
307 fg: cyan
308 bold: true
309 "#;
310 let value: serde_yaml::Value = serde_yaml::from_str(yaml).unwrap();
311 let def = StyleDefinition::parse(&value, "test").unwrap();
312
313 match def {
314 StyleDefinition::Attributes { base, light, dark } => {
315 assert_eq!(base.fg, Some(ColorDef::Named(Color::Cyan)));
316 assert_eq!(base.bold, Some(true));
317 assert!(light.is_none());
318 assert!(dark.is_none());
319 }
320 _ => panic!("Expected Attributes"),
321 }
322 }
323
324 #[test]
325 fn test_parse_mapping_with_light_dark() {
326 let yaml = r#"
327 fg: gray
328 bold: true
329 light:
330 fg: black
331 dark:
332 fg: white
333 "#;
334 let value: serde_yaml::Value = serde_yaml::from_str(yaml).unwrap();
335 let def = StyleDefinition::parse(&value, "test").unwrap();
336
337 match def {
338 StyleDefinition::Attributes { base, light, dark } => {
339 assert_eq!(base.fg, Some(ColorDef::Named(Color::White))); assert_eq!(base.bold, Some(true));
341
342 let light = light.expect("light should be Some");
343 assert_eq!(light.fg, Some(ColorDef::Named(Color::Black)));
344 assert!(light.bold.is_none()); let dark = dark.expect("dark should be Some");
347 assert_eq!(dark.fg, Some(ColorDef::Named(Color::White)));
348 assert!(dark.bold.is_none()); }
350 _ => panic!("Expected Attributes"),
351 }
352 }
353
354 #[test]
355 fn test_parse_mapping_only_light() {
356 let yaml = r#"
357 fg: gray
358 light:
359 fg: black
360 "#;
361 let value: serde_yaml::Value = serde_yaml::from_str(yaml).unwrap();
362 let def = StyleDefinition::parse(&value, "test").unwrap();
363
364 match def {
365 StyleDefinition::Attributes { light, dark, .. } => {
366 assert!(light.is_some());
367 assert!(dark.is_none());
368 }
369 _ => panic!("Expected Attributes"),
370 }
371 }
372
373 #[test]
374 fn test_parse_mapping_only_dark() {
375 let yaml = r#"
376 fg: gray
377 dark:
378 fg: white
379 "#;
380 let value: serde_yaml::Value = serde_yaml::from_str(yaml).unwrap();
381 let def = StyleDefinition::parse(&value, "test").unwrap();
382
383 match def {
384 StyleDefinition::Attributes { light, dark, .. } => {
385 assert!(light.is_none());
386 assert!(dark.is_some());
387 }
388 _ => panic!("Expected Attributes"),
389 }
390 }
391
392 #[test]
397 fn test_parse_empty_string_error() {
398 let value = serde_yaml::Value::String("".into());
399 let result = StyleDefinition::parse(&value, "test");
400 assert!(result.is_err());
401 }
402
403 #[test]
404 fn test_parse_whitespace_only_error() {
405 let value = serde_yaml::Value::String(" ".into());
406 let result = StyleDefinition::parse(&value, "test");
407 assert!(result.is_err());
408 }
409
410 #[test]
411 fn test_parse_invalid_type_error() {
412 let value = serde_yaml::Value::Number(42.into());
413 let result = StyleDefinition::parse(&value, "test");
414 assert!(result.is_err());
415 }
416
417 #[test]
418 fn test_parse_light_not_mapping_error() {
419 let yaml = r#"
420 fg: cyan
421 light: invalid
422 "#;
423 let value: serde_yaml::Value = serde_yaml::from_str(yaml).unwrap();
424 let result = StyleDefinition::parse(&value, "test");
425 assert!(matches!(
426 result,
427 Err(StylesheetError::InvalidDefinition { .. })
428 ));
429 }
430
431 #[test]
436 fn test_is_alias_true() {
437 let def = StyleDefinition::Alias("target".into());
438 assert!(def.is_alias());
439 assert_eq!(def.alias_target(), Some("target"));
440 }
441
442 #[test]
443 fn test_is_alias_false() {
444 let def = StyleDefinition::Attributes {
445 base: StyleAttributes::new(),
446 light: None,
447 dark: None,
448 };
449 assert!(!def.is_alias());
450 assert!(def.alias_target().is_none());
451 }
452
453 #[test]
458 fn test_is_likely_alias_true() {
459 assert!(is_likely_alias("muted"));
460 assert!(is_likely_alias("accent"));
461 assert!(is_likely_alias("my_style"));
462 assert!(is_likely_alias("headerStyle"));
463 }
464
465 #[test]
466 fn test_is_likely_alias_false_for_colors() {
467 assert!(!is_likely_alias("red"));
468 assert!(!is_likely_alias("cyan"));
469 assert!(!is_likely_alias("bright_red"));
470 }
471
472 #[test]
473 fn test_is_likely_alias_false_for_attributes() {
474 assert!(!is_likely_alias("bold"));
475 assert!(!is_likely_alias("italic"));
476 assert!(!is_likely_alias("dim"));
477 }
478}