1use std::collections::HashMap;
4use std::io::{self, BufRead};
5
6use crate::HexColorError;
7
8pub type RawSlots = HashMap<String, String>;
12
13pub fn parse_base24(reader: impl io::Read) -> io::Result<RawSlots> {
20 let mut map = HashMap::new();
21 for line in io::BufReader::new(reader).lines() {
22 let line = line?;
23 let line = line.trim();
24 if line.is_empty() || line.starts_with('#') {
25 continue;
26 }
27 let Some((key, value)) = line.split_once(':') else {
28 continue;
30 };
31 let key = key.trim().to_lowercase();
32 let value = strip_inline_comment(value.trim()).trim().to_string();
33 let value = value.trim_matches('"').trim_matches('\'').to_string();
34 map.insert(key, value);
35 }
36 Ok(map)
37}
38
39fn strip_inline_comment(s: &str) -> &str {
41 let mut in_quote: Option<char> = None;
42 for (i, c) in s.char_indices() {
43 match (in_quote, c) {
44 (None, '"') | (None, '\'') => in_quote = Some(c),
45 (Some(q), c) if c == q => in_quote = None,
46 (None, '#') => return &s[..i],
47 _ => {}
48 }
49 }
50 s
51}
52
53pub fn normalize_hex(s: &str) -> Result<String, HexColorError> {
57 let hex = s.trim_start_matches('#');
58 if hex.len() != 6 {
59 return Err(HexColorError::InvalidLength(hex.len()));
60 }
61 u32::from_str_radix(hex, 16).map_err(HexColorError::InvalidHex)?;
63 Ok(format!("#{}", hex.to_lowercase()))
64}
65
66pub fn is_dark(base00: &str, base07: &str) -> bool {
71 fn brightness(hex: &str) -> u32 {
72 let Ok((r, g, b)) = crate::hex_to_rgb(hex) else {
73 return 0;
74 };
75 r as u32 + g as u32 + b as u32
76 }
77 brightness(base00) < brightness(base07)
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn parse_simple_key_value() {
86 let input = b"scheme: \"My Theme\"\nauthor: \"Ada\"\nbase00: \"1e1e2e\"\n";
87 let slots = parse_base24(&input[..]).unwrap();
88 assert_eq!(slots.get("scheme").map(String::as_str), Some("My Theme"));
89 assert_eq!(slots.get("author").map(String::as_str), Some("Ada"));
90 assert_eq!(slots.get("base00").map(String::as_str), Some("1e1e2e"));
91 }
92
93 #[test]
94 fn parse_skips_blank_lines_and_comments() {
95 let input = b"# comment\n\nscheme: \"X\"\n";
96 let slots = parse_base24(&input[..]).unwrap();
97 assert_eq!(slots.len(), 1);
98 }
99
100 #[test]
101 fn parse_strips_inline_comment() {
102 let input = b"base00: \"aabbcc\" # background\n";
103 let slots = parse_base24(&input[..]).unwrap();
104 assert_eq!(slots.get("base00").map(String::as_str), Some("aabbcc"));
105 }
106
107 #[test]
108 fn parse_keys_are_lowercased() {
109 let input = b"BASE00: \"aabbcc\"\n";
110 let slots = parse_base24(&input[..]).unwrap();
111 assert!(slots.contains_key("base00"));
112 }
113
114 #[test]
115 fn parse_document_separator_is_skipped() {
116 let input = b"---\nscheme: \"Y\"\n";
117 let slots = parse_base24(&input[..]).unwrap();
118 assert_eq!(slots.len(), 1);
119 }
120
121 #[test]
122 fn normalize_hex_with_hash() {
123 assert_eq!(normalize_hex("#FF5533").unwrap(), "#ff5533");
124 }
125
126 #[test]
127 fn normalize_hex_without_hash() {
128 assert_eq!(normalize_hex("FF5533").unwrap(), "#ff5533");
129 }
130
131 #[test]
132 fn normalize_hex_wrong_length() {
133 assert!(normalize_hex("fff").is_err());
134 assert!(normalize_hex("ff553300").is_err());
135 }
136
137 #[test]
138 fn is_dark_dark_theme() {
139 assert!(is_dark("#1e1e2e", "#cdd6f4")); }
141
142 #[test]
143 fn is_dark_light_theme() {
144 assert!(!is_dark("#fdf6e3", "#657b83")); }
146}