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