1use yog_data::Schema;
4use yog_ids::res_kind;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Schema)]
8pub enum Kind {
9 String,
10 Image,
11 File,
12 Sound,
13 Color,
14 Number,
15 Bool,
16}
17
18impl Kind {
19 pub fn name(self) -> &'static str {
20 match self {
21 Kind::String => res_kind::STRING,
22 Kind::Image => res_kind::IMAGE,
23 Kind::File => res_kind::FILE,
24 Kind::Sound => res_kind::SOUND,
25 Kind::Color => res_kind::COLOR,
26 Kind::Number => res_kind::NUMBER,
27 Kind::Bool => res_kind::BOOL,
28 }
29 }
30}
31
32#[derive(Schema)]
34pub struct EntryObject {
35 #[data(rename = "type")]
37 pub kind: Option<Kind>,
38 pub value: yog_data::Value,
40 pub comment: Option<String>,
42 pub key: Option<String>,
44 pub event: Option<String>,
46 pub subtitle: Option<String>,
48}
49
50pub fn split_file_name(file_name: &str) -> Option<(&str, Option<&str>)> {
52 let stem = file_name
53 .strip_suffix(yog_ids::Format::Res.ext())?
54 .strip_suffix('.')?;
55 Some(match stem.split_once('.') {
56 Some((set, locale)) => (set, Some(locale)),
57 None => (stem, None),
58 })
59}
60
61#[must_use]
66pub fn image_texture(rel: &str) -> Option<String> {
67 rel.strip_suffix(yog_ids::res::PNG).map(str::to_string)
68}
69
70use std::collections::HashMap;
73
74use yog_data::{Reader, Seg, Shape, ValueRef};
75
76pub fn kind_of(value: &Value) -> Kind {
78 match value {
79 Value::Str(_) => Kind::String,
80 Value::Num(_) => Kind::Number,
81 Value::Bool(_) => Kind::Bool,
82 Value::Bytes(b) if b.starts_with(OGG_MAGIC) => Kind::Sound,
83 Value::Bytes(b) if PICTURE_MAGIC.iter().any(|m| b.starts_with(m)) => Kind::Image,
84 Value::Bytes(_) => Kind::File,
85 }
86}
87
88pub const OGG_MAGIC: &[u8] = b"OggS";
90const PICTURE_MAGIC: &[&[u8]] = &[b"\x89PNG", b"\xFF\xD8\xFF", b"GIF8", b"RIFF", b"BM"];
92
93#[derive(Debug, Clone, PartialEq, Eq)]
96pub enum Value {
97 Str(String),
98 Num(String),
99 Bool(bool),
100 Bytes(Vec<u8>),
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct Entry {
106 pub name: String,
107 pub kind: Kind,
108 pub value: Value,
109 pub comment: Option<String>,
110 pub key: Option<String>,
111 pub event: Option<String>,
112 pub subtitle: Option<String>,
113 pub line: usize,
115}
116
117impl Entry {
118 pub fn text(&self) -> &str {
120 match &self.value {
121 Value::Str(s) => s,
122 _ => "",
123 }
124 }
125
126 pub fn picture(name: &str, bytes: Vec<u8>) -> Entry {
128 Entry {
129 name: name.to_string(),
130 kind: Kind::Image,
131 value: Value::Bytes(bytes),
132 comment: None,
133 key: None,
134 event: None,
135 subtitle: None,
136 line: 0,
137 }
138 }
139}
140
141const BYTES_PREFIX: &str = yog_ids::concatcp!(
143 yog_ids::sep::TYPED,
144 yog_ids::typed::BYTES,
145 yog_ids::sep::NAMESPACE
146);
147
148pub struct YresFile(Vec<Entry>);
150
151fn scalar(v: &ValueRef, cx: &mut Reader) -> Option<Value> {
153 Some(match v {
154 ValueRef::Bytes(b) => Value::Bytes(b.to_vec()),
155 ValueRef::Str(s) if s.starts_with(BYTES_PREFIX) => match yog_data::typed::classify(s) {
156 Ok(yog_data::Value::Bytes(b)) => Value::Bytes(b),
157 Ok(other) => {
158 cx.problem(format!("`{s}`: bytes, not {other:?}"));
159 return None;
160 }
161 Err(e) => {
162 cx.problem(e);
163 return None;
164 }
165 },
166 ValueRef::Str(_) => Value::Str(String::read(v, cx)?),
167 ValueRef::Int(n) => Value::Num(n.to_string()),
168 ValueRef::Float(f) => Value::Num(format!("{f:?}")),
169 ValueRef::Bool(b) => Value::Bool(*b),
170 other => {
171 cx.expected("a string, a number or true/false", other);
172 return None;
173 }
174 })
175}
176
177fn read_entry(name: &str, v: &ValueRef, cx: &mut Reader) -> Option<Entry> {
178 let mut entry = Entry {
179 name: name.to_string(),
180 kind: Kind::String,
181 value: Value::Str(String::new()),
182 comment: None,
183 key: None,
184 event: None,
185 subtitle: None,
186 line: 0,
187 };
188 if !matches!(v, ValueRef::Map(_)) {
189 entry.value = scalar(v, cx)?;
190 entry.kind = kind_of(&entry.value);
191 return Some(entry);
192 }
193 let o = EntryObject::read(v, cx);
194 let value = v
196 .get("value")
197 .and_then(|val| cx.at(Seg::Key("value".into()), |cx| scalar(val, cx)));
198 let (o, value) = (o?, value?);
199 let kind = o.kind.unwrap_or(kind_of(&value));
200 let fits = matches!(
201 (kind, &value),
202 (Kind::Number, Value::Num(_))
203 | (Kind::Bool, Value::Bool(_))
204 | (Kind::String | Kind::Color, Value::Str(_))
205 | (Kind::Image | Kind::File | Kind::Sound, Value::Bytes(_))
206 );
207 if !fits {
208 let needs = match kind {
209 Kind::Number => "number",
210 Kind::Bool => "true/false",
211 Kind::Image | Kind::File | Kind::Sound => "bytes (`@bytes:…`)",
212 _ => "string",
213 };
214 cx.at(Seg::Key("value".into()), |cx| {
215 cx.problem(format!("a {} needs a {needs} value", kind.name()))
216 });
217 return None;
218 }
219 let misplaced = [
220 ("key", o.key.is_some(), Kind::String),
221 ("event", o.event.is_some(), Kind::Sound),
222 ("subtitle", o.subtitle.is_some(), Kind::Sound),
223 ];
224 for (field, present, only) in misplaced {
225 if present && kind != only {
226 cx.at(Seg::Key(field.into()), |cx| {
227 cx.problem(format!("only {} entries have a `{field}`", only.name()))
228 });
229 return None;
230 }
231 }
232 entry.kind = kind;
233 entry.value = value;
234 (entry.comment, entry.key, entry.event, entry.subtitle) =
235 (o.comment, o.key, o.event, o.subtitle);
236 Some(entry)
237}
238
239impl Schema for YresFile {
240 fn shape() -> Shape {
241 Shape::Map(EntryObject::shape)
242 }
243
244 fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
245 let ValueRef::Map(map) = v else {
246 cx.problem("expected `{` — a .yres file is one map of named resources");
247 return None;
248 };
249 let mut entries = Vec::with_capacity(map.len());
250 let mut ok = true;
251 for (name, v) in map.iter().filter(|(k, _)| !k.starts_with('$')) {
253 match cx.at(Seg::Key(name.to_string()), |cx| read_entry(name, v, cx)) {
254 Some(e) => entries.push(e),
255 None => ok = false,
256 }
257 }
258 ok.then_some(YresFile(entries))
259 }
260
261 fn write(&self) -> yog_data::Value {
262 let mut map = yog_data::Map::new();
263 for e in &self.0 {
264 let value = match &e.value {
265 Value::Str(s) => yog_data::Value::Str(s.clone()),
266 Value::Num(n) => n
267 .parse::<i64>()
268 .map(yog_data::Value::Int)
269 .unwrap_or_else(|_| yog_data::Value::Float(n.parse().unwrap_or_default())),
270 Value::Bool(b) => yog_data::Value::Bool(*b),
271 Value::Bytes(b) => yog_data::Value::Bytes(b.clone()),
272 };
273 let plain = e.kind == kind_of(&e.value)
274 && e.comment.is_none()
275 && e.key.is_none()
276 && e.event.is_none()
277 && e.subtitle.is_none();
278 if plain {
279 map.insert(e.name.clone(), value);
280 continue;
281 }
282 let o = EntryObject {
283 kind: (e.kind != kind_of(&e.value)).then_some(e.kind),
284 value,
285 comment: e.comment.clone(),
286 key: e.key.clone(),
287 event: e.event.clone(),
288 subtitle: e.subtitle.clone(),
289 };
290 map.insert(e.name.clone(), o.write());
291 }
292 yog_data::Value::Map(map)
293 }
294}
295
296pub fn parse(text: &str) -> Result<Vec<Entry>, String> {
299 let doc = yog_data::text::parse(text).map_err(|e| e.to_string())?;
300 let mut entries = yog_data::schema::from_doc::<YresFile>(text, &doc)
301 .map_err(|e| e.to_string())?
302 .value
303 .0;
304 let mut lines = HashMap::new();
306 if let yog_data::Value::Map(map) = &doc.value {
307 let mut line = 1;
308 let mut at = 0;
309 for ((name, _), spans) in map.iter().zip(&doc.spans.children) {
310 let start = spans.key.unwrap_or(spans.span).start as usize;
311 line += text.as_bytes()[at..start]
312 .iter()
313 .filter(|&&b| b == b'\n')
314 .count();
315 at = start;
316 lines.insert(name, line);
317 }
318 }
319 for e in &mut entries {
320 e.line = lines.get(e.name.as_str()).copied().unwrap_or(0);
321 }
322 Ok(entries)
323}
324
325pub fn write(entries: &[Entry]) -> String {
327 yog_data::text::to_string_pretty(&YresFile(entries.to_vec()).write())
328}