use std::collections::HashSet;
use std::fmt::Write as _;
use yog_gfx::image::ImageSource;
use super::{Element, err};
pub(crate) fn number(el: &Element, key: &str, value: &str) -> Result<f32, String> {
value
.trim()
.parse::<f32>()
.or_else(|_| err(el, format_args!("{key}=\"{value}\": expected a number")))
}
pub(crate) fn boolean(el: &Element, key: &str, value: &str) -> Result<bool, String> {
match value.trim() {
yog_ids::yui_value::TRUE => Ok(true),
yog_ids::yui_value::FALSE => Ok(false),
_ => err(
el,
format_args!("{key}=\"{value}\": expected true or false"),
),
}
}
pub fn color(value: &str) -> Option<u32> {
yog_ui::attr::written_color(value).map(|c| c.0)
}
pub const HEX_MARK: &str = yog_ui::attr::HEX_MARKS[0];
pub(crate) fn color_attr(el: &Element, key: &str, value: &str) -> Result<u32, String> {
if let Some(reference) = value.trim().strip_prefix('@') {
return resource_color(reference).ok_or_else(|| {
format!(
"line {}: <{}> {key}=\"{value}\": no color `{reference}` in resources/*.yres",
el.line, el.name
)
});
}
color(value).ok_or_else(|| {
format!(
"line {}: <{}> {key}=\"{value}\": expected #RRGGBB, #AARRGGBB or @<Set>.<name>",
el.line, el.name
)
})
}
pub(super) fn is_resource_text(reference: &str) -> bool {
RESOURCE_REFS.with(|r| r.borrow().texts.contains_key(reference.trim()))
}
pub(super) fn resource_color(reference: &str) -> Option<u32> {
RESOURCE_REFS.with(|r| r.borrow().colors.get(reference.trim()).copied())
}
pub(crate) fn snake(name: &str) -> String {
let mut out = String::new();
for (i, c) in name.chars().enumerate() {
if c.is_ascii_uppercase() {
if i > 0 {
out.push('_');
}
out.push(c.to_ascii_lowercase());
} else {
out.push(c);
}
}
out
}
pub(super) fn pick<'v>(
el: &Element,
key: &str,
value: &str,
options: &[&'v str],
) -> Result<&'v str, String> {
options
.iter()
.find(|o| **o == value.trim())
.copied()
.ok_or_else(|| {
format!(
"line {}: <{}> {key}=\"{value}\": expected one of {}",
el.line,
el.name,
options.join(", ")
)
})
}
pub fn pascal(value: &str) -> String {
value
.split(['_', '-'])
.map(|w| {
let mut c = w.chars();
c.next()
.map(|f| f.to_uppercase().collect::<String>() + c.as_str())
.unwrap_or_default()
})
.collect()
}
pub fn text_expr(value: &str) -> Result<String, String> {
if value.trim() == "@" || !value.starts_with('@') {
return Ok(format!("{value:?}"));
}
let tr = |key: &str| format!("::yog_api::tr({key:?})");
match yog_data::typed::classify(value.trim()) {
Ok(yog_data::Value::Str(s)) => Ok(format!("{s:?}")),
Ok(yog_data::Value::Lang(key)) => Ok(tr(&key)),
Ok(yog_data::Value::Res(r)) => {
RESOURCE_REFS.with(|refs| refs.borrow().text(&r).map(|(k, _)| tr(k)))
}
Ok(yog_data::Value::Ref(r))
if matches!(
r.kind.as_str(),
yog_ids::kind::ITEM | yog_ids::kind::BLOCK | yog_ids::kind::ENTITY
) =>
{
let id = r.id.split(STATE_OPEN).next().unwrap_or(&r.id);
Ok(tr(&yog_ids::lang_key(&r.kind, id)))
}
Ok(other) => Err(format!("{} isn't a text", other.kind_name())),
Err(_) if !value.contains(':') => Err(format!(
"write a translation key as `@lang:{}`, a .yres text as `@Set.name`, a literal `@` as `@@`",
value[1..].trim()
)),
Err(e) => Err(e),
}
}
const STATE_OPEN: char = '[';
pub(crate) fn text_attr(el: &Element, key: &str, value: &str) -> Result<String, String> {
text_expr(value).map_err(|e| format!("line {}: <{}> {key}=\"{value}\": {e}", el.line, el.name))
}
thread_local! {
static RESOURCE_REFS: std::cell::RefCell<crate::yres::Refs> = Default::default();
}
pub fn set_resource_refs(refs: &crate::yres::Refs) {
RESOURCE_REFS.with(|r| *r.borrow_mut() = refs.clone());
}
pub fn const_name(value: &str) -> String {
let mut out = String::new();
let mut prev_lower = false;
for c in value.chars() {
if c.is_ascii_alphanumeric() {
if c.is_ascii_uppercase() && prev_lower {
out.push('_');
}
prev_lower = c.is_ascii_lowercase() || c.is_ascii_digit();
out.push(c.to_ascii_uppercase());
} else {
if !out.ends_with('_') {
out.push('_');
}
prev_lower = false;
}
}
let out = out.trim_matches('_').to_string();
if out.is_empty() || out.starts_with(|c: char| c.is_ascii_digit()) {
format!("_{out}")
} else {
out
}
}
#[derive(Debug, Default)]
pub(crate) struct Names {
list: Vec<String>,
seen: HashSet<String>,
}
impl Names {
pub(crate) fn push(&mut self, name: &str) {
if !self.seen.contains(name) {
self.seen.insert(name.to_string());
self.list.push(name.to_string());
}
}
pub(crate) fn as_slice(&self) -> &[String] {
&self.list
}
pub(crate) fn into_vec(self) -> Vec<String> {
self.list
}
}
pub(crate) fn str_consts(values: &[String]) -> String {
let mut out = String::new();
let mut used: HashSet<String> = HashSet::new();
for v in values {
let mut name = const_name(v);
while used.contains(&name) {
name.push('_');
}
let _ = writeln!(out, " pub const {name}: &str = {v:?};");
used.insert(name);
}
out
}
pub(crate) fn is_image_value(v: &str) -> bool {
let v = v.trim();
match v.strip_prefix('@') {
Some(name) => RESOURCE_REFS.with(|r| r.borrow().images.contains_key(name)),
None => !v.starts_with('#') && v.contains(':'),
}
}
pub(crate) fn image_source(
el: &Element,
attr: &str,
source: &str,
mod_id: Option<&str>,
) -> Result<String, String> {
let line = el.line;
let kind = el.name.as_str();
let mod_id = mod_id.map(str::to_owned);
let resolved;
let source = match source.strip_prefix('@') {
Some(name) => {
resolved = RESOURCE_REFS
.with(|r| r.borrow().images.get(name).cloned())
.ok_or_else(|| format!("line {line}: <{kind}> {attr}=\"{source}\": no image `{name}` in resources/*.yres"))?;
resolved.as_str()
}
None => source,
};
let parsed = ImageSource::parse(source).ok_or_else(|| {
format!(
"line {line}: <{kind}> {attr}=\"{source}\": expected item:<id>, block:<id>, \
sprite:<name>, mod:<path> or <namespace>:<path>"
)
})?;
let source = match parsed {
ImageSource::ModPath(path) => match &mod_id {
Some(id) => yog_gfx::image::texture_id(id, &path),
None => {
return Err(format!(
"line {line}: <{kind}> {attr}=\"{source}\": mod: needs the mod id (no yog.toml found)"
));
}
},
ImageSource::Sprite(name)
if !name.contains(':') && yog_gfx::image::sprite(&name).is_none() =>
{
let names: Vec<&str> = yog_gfx::image::SPRITES
.iter()
.map(|s| s.name.as_str())
.collect();
return Err(format!(
"line {line}: <{kind}> {attr}=\"{source}\": unknown sprite (known: {})",
names.join(", ")
));
}
ImageSource::Texture(_) => {
if let Some(alias) = yog_gfx::image::suggest_alias(source) {
return Err(format!(
"line {line}: <{kind}> {attr}=\"{source}\" is a raw resource path, which \
changes between Minecraft versions — write {attr}=\"{alias}\""
));
}
source.to_string()
}
_ => source.to_string(),
};
Ok(source)
}
pub(super) fn font_attr<'a>(el: &Element, v: &'a str) -> Result<&'a str, String> {
let font = v.trim();
match yog_ids::split(font) {
(yog_ids::ns::MINECRAFT, name) if !font.is_empty() => err(
el,
format!(
"font=\"{font}\": the game's fonts are named by Yog's alias, {}",
yog_ids::font_alias(name)
),
),
_ => Ok(font),
}
}