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