1use std::collections::BTreeMap;
4
5use toml::de::{DeTable, DeValue};
6
7use super::{
8 AnimationFrame, CellAnimation, CellColor, ColorMode, FrameTime, MAX_FRAMES, Playback, check_glyph, is_valid_name,
9};
10use crate::diagnostics::Diagnostic;
11use crate::doc::{self, Doc, Value};
12use crate::icons::GlyphMode;
13
14const ANIMATION_KEYS: [&str; 5] = ["frame", "playback", "colors", "rest", "frames"];
16
17const FRAME_KEYS: [&str; 5] = ["nerd", "unicode", "ascii", "color", "duration"];
19
20pub(crate) fn read_animation_table(
23 doc: &Doc<'_>,
24 table: &DeTable<'_>,
25 animations: &mut BTreeMap<String, CellAnimation>,
26 report: &mut Vec<Diagnostic>,
27) {
28 for (key, value) in table {
29 let name = key.get_ref();
30 if !is_valid_name(name) {
31 report.push(
32 doc.error(
33 &key.span(),
34 format!("animation name `{name}` may use only lowercase letters, digits and `-`"),
35 ),
36 );
37 continue;
38 }
39 match read_animation(doc, name, value) {
40 Ok(animation) => {
41 animations.insert(name.to_string(), animation);
42 }
43 Err(diagnostic) => report.push(diagnostic),
44 }
45 }
46}
47
48#[must_use]
52pub fn parse_animations(file: &str, text: &str) -> (Vec<(String, CellAnimation)>, Vec<Diagnostic>) {
53 let doc = Doc::new(file, text);
54 let mut report = Vec::new();
55 let root = match doc.parse() {
56 Ok(root) => root,
57 Err(diagnostic) => return (Vec::new(), vec![diagnostic]),
58 };
59 let mut found = Vec::new();
60 for (key, value) in &root {
61 if key.get_ref() != "animations" {
62 report.push(doc.error(&key.span(), format!("unknown section `{}`; expected animations", key.get_ref())));
63 continue;
64 }
65 let table = match doc.table(value, "animations") {
66 Ok(table) => table,
67 Err(diagnostic) => {
68 report.push(diagnostic);
69 continue;
70 }
71 };
72 let mut animations = BTreeMap::new();
73 read_animation_table(&doc, table, &mut animations, &mut report);
74 for (name, _) in table {
76 if let Some(animation) = animations.remove(name.get_ref().as_ref()) {
77 found.push((name.get_ref().to_string(), animation));
78 }
79 }
80 }
81 (found, report)
82}
83
84fn read_animation(doc: &Doc<'_>, name: &str, value: &Value<'_>) -> Result<CellAnimation, Diagnostic> {
85 let table = doc.table(value, &format!("animation `{name}`"))?;
86 if let Some((unknown, entry)) = table.iter().find(|(key, _)| !ANIMATION_KEYS.contains(&key.get_ref().as_ref())) {
87 return Err(doc.error(
88 &entry.span(),
89 format!(
90 "animation `{name}` has unknown key `{}`; expected one of: {}",
91 unknown.get_ref(),
92 ANIMATION_KEYS.join(", ")
93 ),
94 ));
95 }
96 let mut animation = CellAnimation::new();
97 if let Some(entry) = doc::get(table, "frame") {
98 let text = doc.string(entry, &format!("animation `{name}`.frame"))?;
99 let time = FrameTime::parse(text).map_err(|message| doc.error(&entry.span(), message))?;
100 animation = animation.frame_time(time);
101 }
102 if let Some(entry) = doc::get(table, "playback") {
103 let text = doc.string(entry, &format!("animation `{name}`.playback"))?;
104 let playback = Playback::from_name(text).ok_or_else(|| {
105 doc.error(&entry.span(), format!("animation `{name}`.playback is `{text}`; use loop, once or bounce"))
106 })?;
107 animation = animation.playback(playback);
108 }
109 if let Some(entry) = doc::get(table, "colors") {
110 let text = doc.string(entry, &format!("animation `{name}`.colors"))?;
111 let colors = ColorMode::from_name(text).ok_or_else(|| {
112 doc.error(&entry.span(), format!("animation `{name}`.colors is `{text}`; use step or blend"))
113 })?;
114 animation = animation.colors(colors);
115 }
116 let Some(frames) = doc::get(table, "frames") else {
117 return Err(doc.error(&value.span(), format!("animation `{name}` has no `frames`")));
118 };
119 let DeValue::Array(items) = frames.get_ref() else {
120 return Err(doc.error(
121 &frames.span(),
122 format!("animation `{name}`.frames must be an array of frames, found {}", frames.get_ref().type_str()),
123 ));
124 };
125 if items.is_empty() {
126 return Err(doc.error(&frames.span(), format!("animation `{name}` needs at least one frame")));
127 }
128 if items.len() > MAX_FRAMES {
129 return Err(doc.error(
130 &frames.span(),
131 format!("animation `{name}` has {} frames; at most {MAX_FRAMES} are allowed", items.len()),
132 ));
133 }
134 for (index, item) in items.iter().enumerate() {
135 animation = animation.frame(read_frame(doc, &format!("animation `{name}` frame {}", index + 1), item)?);
136 }
137 if let Some(entry) = doc::get(table, "rest") {
138 let count = items.len();
139 let rest = integer(entry.get_ref())
140 .and_then(|rest| usize::try_from(rest).ok())
141 .filter(|rest| (1..=count).contains(rest))
142 .ok_or_else(|| {
143 doc.error(&entry.span(), format!("animation `{name}`.rest must be a frame number from 1 to {count}"))
144 })?;
145 animation = animation.rest(rest - 1);
146 }
147 Ok(animation)
148}
149
150fn read_frame(doc: &Doc<'_>, what: &str, value: &Value<'_>) -> Result<AnimationFrame, Diagnostic> {
151 let table = doc.table(value, what)?;
152 if let Some((unknown, entry)) = table.iter().find(|(key, _)| !FRAME_KEYS.contains(&key.get_ref().as_ref())) {
153 return Err(doc.error(
154 &entry.span(),
155 format!("{what} has unknown key `{}`; expected one of: {}", unknown.get_ref(), FRAME_KEYS.join(", ")),
156 ));
157 }
158 let glyph = |key: &str, mode: GlyphMode| -> Result<Option<String>, Diagnostic> {
159 let Some(entry) = doc::get(table, key) else {
160 return Ok(None);
161 };
162 let text = doc.string(entry, &format!("{what}.{key}"))?;
163 check_glyph(text, mode).map_err(|message| doc.error(&entry.span(), format!("{what}.{key}: {message}")))?;
164 Ok(Some(text.to_owned()))
165 };
166 let Some(ascii) = glyph("ascii", GlyphMode::Ascii)? else {
167 return Err(doc.error(&value.span(), format!("{what} is missing its `ascii` glyph, which every frame needs")));
168 };
169 let mut frame = AnimationFrame::new(ascii);
170 if let Some(unicode) = glyph("unicode", GlyphMode::Unicode)? {
171 frame = frame.unicode(unicode);
172 }
173 if let Some(nerd) = glyph("nerd", GlyphMode::Nerd)? {
174 frame = frame.nerd(nerd);
175 }
176 if let Some(entry) = doc::get(table, "color") {
177 let text = doc.string(entry, &format!("{what}.color"))?;
178 let color = CellColor::parse(text).map_err(|message| doc.error(&entry.span(), format!("{what}: {message}")))?;
179 frame = frame.color(color);
180 }
181 if let Some(entry) = doc::get(table, "duration") {
182 let text = doc.string(entry, &format!("{what}.duration"))?;
183 let time = FrameTime::parse(text).map_err(|message| doc.error(&entry.span(), format!("{what}: {message}")))?;
184 frame = frame.duration(time);
185 }
186 Ok(frame)
187}
188
189fn integer(value: &DeValue<'_>) -> Option<i64> {
191 let int = value.as_integer()?;
192 i64::from_str_radix(int.as_str(), int.radix()).ok()
193}