use yog_data::Schema;
use yog_ids::res_kind;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Schema)]
pub enum Kind {
String,
Image,
File,
Sound,
Color,
Number,
Bool,
}
impl Kind {
pub fn name(self) -> &'static str {
match self {
Kind::String => res_kind::STRING,
Kind::Image => res_kind::IMAGE,
Kind::File => res_kind::FILE,
Kind::Sound => res_kind::SOUND,
Kind::Color => res_kind::COLOR,
Kind::Number => res_kind::NUMBER,
Kind::Bool => res_kind::BOOL,
}
}
}
#[derive(Schema)]
pub struct EntryObject {
#[data(rename = "type")]
pub kind: Option<Kind>,
pub value: yog_data::Value,
pub comment: Option<String>,
pub key: Option<String>,
pub event: Option<String>,
pub subtitle: Option<String>,
}
pub fn split_file_name(file_name: &str) -> Option<(&str, Option<&str>)> {
let stem = file_name
.strip_suffix(yog_ids::Format::Res.ext())?
.strip_suffix('.')?;
Some(match stem.split_once('.') {
Some((set, locale)) => (set, Some(locale)),
None => (stem, None),
})
}
#[must_use]
pub fn image_texture(rel: &str) -> Option<String> {
rel.strip_suffix(yog_ids::res::PNG).map(str::to_string)
}
use std::collections::HashMap;
use yog_data::{Reader, Seg, Shape, ValueRef};
pub fn kind_of(value: &Value) -> Kind {
match value {
Value::Str(_) => Kind::String,
Value::Num(_) => Kind::Number,
Value::Bool(_) => Kind::Bool,
Value::Bytes(b) if b.starts_with(OGG_MAGIC) => Kind::Sound,
Value::Bytes(b) if PICTURE_MAGIC.iter().any(|m| b.starts_with(m)) => Kind::Image,
Value::Bytes(_) => Kind::File,
}
}
pub const OGG_MAGIC: &[u8] = b"OggS";
const PICTURE_MAGIC: &[&[u8]] = &[b"\x89PNG", b"\xFF\xD8\xFF", b"GIF8", b"RIFF", b"BM"];
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Value {
Str(String),
Num(String),
Bool(bool),
Bytes(Vec<u8>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Entry {
pub name: String,
pub kind: Kind,
pub value: Value,
pub comment: Option<String>,
pub key: Option<String>,
pub event: Option<String>,
pub subtitle: Option<String>,
pub line: usize,
}
impl Entry {
pub fn text(&self) -> &str {
match &self.value {
Value::Str(s) => s,
_ => "",
}
}
pub fn picture(name: &str, bytes: Vec<u8>) -> Entry {
Entry {
name: name.to_string(),
kind: Kind::Image,
value: Value::Bytes(bytes),
comment: None,
key: None,
event: None,
subtitle: None,
line: 0,
}
}
}
const BYTES_PREFIX: &str = yog_ids::concatcp!(
yog_ids::sep::TYPED,
yog_ids::typed::BYTES,
yog_ids::sep::NAMESPACE
);
pub struct YresFile(Vec<Entry>);
fn scalar(v: &ValueRef, cx: &mut Reader) -> Option<Value> {
Some(match v {
ValueRef::Bytes(b) => Value::Bytes(b.to_vec()),
ValueRef::Str(s) if s.starts_with(BYTES_PREFIX) => match yog_data::typed::classify(s) {
Ok(yog_data::Value::Bytes(b)) => Value::Bytes(b),
Ok(other) => {
cx.problem(format!("`{s}`: bytes, not {other:?}"));
return None;
}
Err(e) => {
cx.problem(e);
return None;
}
},
ValueRef::Str(_) => Value::Str(String::read(v, cx)?),
ValueRef::Int(n) => Value::Num(n.to_string()),
ValueRef::Float(f) => Value::Num(format!("{f:?}")),
ValueRef::Bool(b) => Value::Bool(*b),
other => {
cx.expected("a string, a number or true/false", other);
return None;
}
})
}
fn read_entry(name: &str, v: &ValueRef, cx: &mut Reader) -> Option<Entry> {
let mut entry = Entry {
name: name.to_string(),
kind: Kind::String,
value: Value::Str(String::new()),
comment: None,
key: None,
event: None,
subtitle: None,
line: 0,
};
if !matches!(v, ValueRef::Map(_)) {
entry.value = scalar(v, cx)?;
entry.kind = kind_of(&entry.value);
return Some(entry);
}
let o = EntryObject::read(v, cx);
let value = v
.get("value")
.and_then(|val| cx.at(Seg::Key("value".into()), |cx| scalar(val, cx)));
let (o, value) = (o?, value?);
let kind = o.kind.unwrap_or(kind_of(&value));
let fits = matches!(
(kind, &value),
(Kind::Number, Value::Num(_))
| (Kind::Bool, Value::Bool(_))
| (Kind::String | Kind::Color, Value::Str(_))
| (Kind::Image | Kind::File | Kind::Sound, Value::Bytes(_))
);
if !fits {
let needs = match kind {
Kind::Number => "number",
Kind::Bool => "true/false",
Kind::Image | Kind::File | Kind::Sound => "bytes (`@bytes:…`)",
_ => "string",
};
cx.at(Seg::Key("value".into()), |cx| {
cx.problem(format!("a {} needs a {needs} value", kind.name()))
});
return None;
}
let misplaced = [
("key", o.key.is_some(), Kind::String),
("event", o.event.is_some(), Kind::Sound),
("subtitle", o.subtitle.is_some(), Kind::Sound),
];
for (field, present, only) in misplaced {
if present && kind != only {
cx.at(Seg::Key(field.into()), |cx| {
cx.problem(format!("only {} entries have a `{field}`", only.name()))
});
return None;
}
}
entry.kind = kind;
entry.value = value;
(entry.comment, entry.key, entry.event, entry.subtitle) =
(o.comment, o.key, o.event, o.subtitle);
Some(entry)
}
impl Schema for YresFile {
fn shape() -> Shape {
Shape::Map(EntryObject::shape)
}
fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
let ValueRef::Map(map) = v else {
cx.problem("expected `{` — a .yres file is one map of named resources");
return None;
};
let mut entries = Vec::with_capacity(map.len());
let mut ok = true;
for (name, v) in map.iter().filter(|(k, _)| !k.starts_with('$')) {
match cx.at(Seg::Key(name.to_string()), |cx| read_entry(name, v, cx)) {
Some(e) => entries.push(e),
None => ok = false,
}
}
ok.then_some(YresFile(entries))
}
fn write(&self) -> yog_data::Value {
let mut map = yog_data::Map::new();
for e in &self.0 {
let value = match &e.value {
Value::Str(s) => yog_data::Value::Str(s.clone()),
Value::Num(n) => n
.parse::<i64>()
.map(yog_data::Value::Int)
.unwrap_or_else(|_| yog_data::Value::Float(n.parse().unwrap_or_default())),
Value::Bool(b) => yog_data::Value::Bool(*b),
Value::Bytes(b) => yog_data::Value::Bytes(b.clone()),
};
let plain = e.kind == kind_of(&e.value)
&& e.comment.is_none()
&& e.key.is_none()
&& e.event.is_none()
&& e.subtitle.is_none();
if plain {
map.insert(e.name.clone(), value);
continue;
}
let o = EntryObject {
kind: (e.kind != kind_of(&e.value)).then_some(e.kind),
value,
comment: e.comment.clone(),
key: e.key.clone(),
event: e.event.clone(),
subtitle: e.subtitle.clone(),
};
map.insert(e.name.clone(), o.write());
}
yog_data::Value::Map(map)
}
}
pub fn parse(text: &str) -> Result<Vec<Entry>, yog_data::Errors> {
let doc = yog_data::text::parse(text).map_err(yog_data::Errors::from)?;
let mut entries = yog_data::schema::from_doc::<YresFile>(text, &doc)?.value.0;
let mut lines = HashMap::new();
if let yog_data::Value::Map(map) = &doc.value {
let mut line = 1;
let mut at = 0;
for ((name, _), spans) in map.iter().zip(&doc.spans.children) {
let start = spans.key.unwrap_or(spans.span).start as usize;
line += text.as_bytes()[at..start]
.iter()
.filter(|&&b| b == b'\n')
.count();
at = start;
lines.insert(name, line);
}
}
for e in &mut entries {
e.line = lines.get(e.name.as_str()).copied().unwrap_or(0);
}
Ok(entries)
}
pub fn write(entries: &[Entry]) -> String {
yog_data::text::to_string_pretty(&YresFile(entries.to_vec()).write())
}