onedrop_parser/
milk_msg_ini.rs1use crate::error::Result;
35
36#[derive(Debug, Clone, PartialEq)]
38pub struct MessageDef {
39 pub slot: u32,
42 pub text: String,
46 pub font: u32,
51 pub size: f32,
54 pub x: f32,
57 pub y: f32,
58 pub r: u8,
61 pub g: u8,
62 pub b: u8,
63 pub bold: bool,
67 pub italic: bool,
68 pub fade_in_s: f32,
70 pub fade_out_s: f32,
71 pub duration_s: f32,
75 pub burn: bool,
79}
80
81impl Default for MessageDef {
82 fn default() -> Self {
83 Self {
84 slot: 0,
85 text: String::new(),
86 font: 0,
87 size: 32.0,
88 x: 0.5,
89 y: 0.5,
90 r: 255,
91 g: 255,
92 b: 255,
93 bold: false,
94 italic: false,
95 fade_in_s: 1.0,
96 fade_out_s: 1.0,
97 duration_s: 3.0,
98 burn: false,
99 }
100 }
101}
102
103pub fn parse_milk_msg_ini(input: &str) -> Result<Vec<MessageDef>> {
107 let mut out: Vec<MessageDef> = Vec::new();
108 let mut current: Option<MessageDef> = None;
109
110 let flush = |out: &mut Vec<MessageDef>, def: MessageDef| {
111 if !def.text.is_empty() {
112 out.push(def);
113 }
114 };
115
116 for raw_line in input.lines() {
117 let line = raw_line.trim();
118 if line.is_empty() || line.starts_with("//") || line.starts_with(';') {
119 continue;
120 }
121
122 if let Some(rest) = line.strip_prefix('[').and_then(|l| l.strip_suffix(']')) {
123 if let Some(def) = current.take() {
124 flush(&mut out, def);
125 }
126 let lower = rest.to_ascii_lowercase();
127 if let Some(num) = lower.strip_prefix("message") {
128 if let Ok(slot) = num.trim_start_matches('0').parse::<u32>() {
129 current = Some(MessageDef {
130 slot,
131 ..Default::default()
132 });
133 } else if num.trim_start_matches('0').is_empty() {
134 current = Some(MessageDef::default());
135 }
136 }
137 continue;
138 }
139
140 let Some(eq) = line.find('=') else { continue };
141 let key = line[..eq].trim().to_ascii_lowercase();
142 let value = line[eq + 1..].trim();
143 let Some(def) = current.as_mut() else {
144 continue;
145 };
146
147 match key.as_str() {
148 "text" => def.text = value.to_string(),
149 "font" => def.font = value.parse().unwrap_or(def.font),
150 "size" => def.size = value.parse().unwrap_or(def.size),
151 "x" => def.x = value.parse().unwrap_or(def.x),
152 "y" => def.y = value.parse().unwrap_or(def.y),
153 "r" => def.r = value.parse().unwrap_or(def.r),
154 "g" => def.g = value.parse().unwrap_or(def.g),
155 "b" => def.b = value.parse().unwrap_or(def.b),
156 "bold" => def.bold = parse_bool(value),
157 "italic" => def.italic = parse_bool(value),
158 "fadein" | "fade_in" => def.fade_in_s = value.parse().unwrap_or(def.fade_in_s),
159 "fadeout" | "fade_out" => def.fade_out_s = value.parse().unwrap_or(def.fade_out_s),
160 "duration" => def.duration_s = value.parse().unwrap_or(def.duration_s),
161 "burn" => def.burn = parse_bool(value),
162 _ => {} }
164 }
165 if let Some(def) = current.take() {
166 flush(&mut out, def);
167 }
168 out.sort_by_key(|m| m.slot);
169 Ok(out)
170}
171
172fn parse_bool(s: &str) -> bool {
173 let lower = s.trim().to_ascii_lowercase();
174 !(lower.is_empty() || lower == "0" || lower == "false" || lower == "no" || lower == "off")
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn parses_complete_message() {
183 let input = "\
184[message01]
185text=Hello world
186font=2
187size=24
188x=0.25
189y=0.75
190r=128
191g=200
192b=255
193bold=1
194italic=0
195fadein=0.5
196fadeout=0.5
197duration=2.0
198burn=1
199";
200 let msgs = parse_milk_msg_ini(input).unwrap();
201 assert_eq!(msgs.len(), 1);
202 let m = &msgs[0];
203 assert_eq!(m.slot, 1);
204 assert_eq!(m.text, "Hello world");
205 assert_eq!(m.font, 2);
206 assert!((m.size - 24.0).abs() < 1e-6);
207 assert!((m.x - 0.25).abs() < 1e-6);
208 assert!((m.y - 0.75).abs() < 1e-6);
209 assert_eq!(m.r, 128);
210 assert_eq!(m.g, 200);
211 assert_eq!(m.b, 255);
212 assert!(m.bold);
213 assert!(!m.italic);
214 assert!((m.fade_in_s - 0.5).abs() < 1e-6);
215 assert!((m.duration_s - 2.0).abs() < 1e-6);
216 assert!(m.burn);
217 }
218
219 #[test]
220 fn defaults_fill_missing_keys() {
221 let input = "\
222[message03]
223text=Just text
224";
225 let msgs = parse_milk_msg_ini(input).unwrap();
226 assert_eq!(msgs.len(), 1);
227 let m = &msgs[0];
228 assert_eq!(m.slot, 3);
229 assert_eq!(m.text, "Just text");
230 assert_eq!(m.font, 0);
231 assert_eq!(m.r, 255);
232 assert_eq!(m.g, 255);
233 assert_eq!(m.b, 255);
234 assert!(!m.bold);
235 assert!(!m.italic);
236 assert!((m.size - 32.0).abs() < 1e-6);
237 }
238
239 #[test]
240 fn drops_messages_with_no_text() {
241 let input = "\
242[message02]
243font=1
244size=20
245";
246 let msgs = parse_milk_msg_ini(input).unwrap();
247 assert!(msgs.is_empty());
248 }
249
250 #[test]
251 fn malformed_numeric_falls_back_to_default() {
252 let input = "\
253[message01]
254text=foo
255size=not-a-number
256r=300
257";
258 let msgs = parse_milk_msg_ini(input).unwrap();
259 assert!((msgs[0].size - 32.0).abs() < 1e-6);
261 assert_eq!(msgs[0].r, 255);
262 }
263
264 #[test]
265 fn sorts_by_slot() {
266 let input = "\
267[message05]
268text=fifth
269[message02]
270text=second
271[message09]
272text=ninth
273";
274 let msgs = parse_milk_msg_ini(input).unwrap();
275 let slots: Vec<u32> = msgs.iter().map(|m| m.slot).collect();
276 assert_eq!(slots, vec![2, 5, 9]);
277 }
278}