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