1use std::collections::HashSet;
5use std::fmt::Write as _;
6
7use yog_gfx::image::ImageSource;
8
9use super::{Element, err};
10
11pub(crate) fn number(el: &Element, key: &str, value: &str) -> Result<f32, String> {
13 value
14 .trim()
15 .parse::<f32>()
16 .or_else(|_| err(el, format_args!("{key}=\"{value}\": expected a number")))
17}
18
19pub(crate) fn boolean(el: &Element, key: &str, value: &str) -> Result<bool, String> {
21 match value.trim() {
22 yog_ids::yui_value::TRUE => Ok(true),
23 yog_ids::yui_value::FALSE => Ok(false),
24 _ => err(
25 el,
26 format_args!("{key}=\"{value}\": expected true or false"),
27 ),
28 }
29}
30
31pub fn color(value: &str) -> Option<u32> {
34 yog_ui::attr::written_color(value).map(|c| c.0)
35}
36
37pub const HEX_MARK: &str = yog_ui::attr::HEX_MARKS[0];
39
40pub(crate) fn color_attr(el: &Element, key: &str, value: &str) -> Result<u32, String> {
43 if let Some(reference) = value.trim().strip_prefix('@') {
44 return resource_color(reference).ok_or_else(|| {
45 format!(
46 "line {}: <{}> {key}=\"{value}\": no color `{reference}` in resources/*.yres",
47 el.line, el.name
48 )
49 });
50 }
51 color(value).ok_or_else(|| {
52 format!(
53 "line {}: <{}> {key}=\"{value}\": expected #RRGGBB, #AARRGGBB or @<Set>.<name>",
54 el.line, el.name
55 )
56 })
57}
58
59pub(super) fn is_resource_text(reference: &str) -> bool {
61 RESOURCE_REFS.with(|r| r.borrow().texts.contains_key(reference.trim()))
62}
63
64pub(super) fn resource_color(reference: &str) -> Option<u32> {
66 RESOURCE_REFS.with(|r| r.borrow().colors.get(reference.trim()).copied())
67}
68
69pub(crate) fn snake(name: &str) -> String {
71 let mut out = String::new();
72 for (i, c) in name.chars().enumerate() {
73 if c.is_ascii_uppercase() {
74 if i > 0 {
75 out.push('_');
76 }
77 out.push(c.to_ascii_lowercase());
78 } else {
79 out.push(c);
80 }
81 }
82 out
83}
84
85pub(super) fn pick<'v>(
87 el: &Element,
88 key: &str,
89 value: &str,
90 options: &[&'v str],
91) -> Result<&'v str, String> {
92 options
93 .iter()
94 .find(|o| **o == value.trim())
95 .copied()
96 .ok_or_else(|| {
97 format!(
98 "line {}: <{}> {key}=\"{value}\": expected one of {}",
99 el.line,
100 el.name,
101 options.join(", ")
102 )
103 })
104}
105
106pub fn pascal(value: &str) -> String {
108 value
109 .split(['_', '-'])
110 .map(|w| {
111 let mut c = w.chars();
112 c.next()
113 .map(|f| f.to_uppercase().collect::<String>() + c.as_str())
114 .unwrap_or_default()
115 })
116 .collect()
117}
118
119pub fn text_expr(value: &str) -> Result<String, String> {
127 if value.trim() == "@" || !value.starts_with('@') {
128 return Ok(format!("{value:?}"));
129 }
130 let tr = |key: &str| format!("::yog_api::tr({key:?})");
131 match yog_data::typed::classify(value.trim()) {
132 Ok(yog_data::Value::Str(s)) => Ok(format!("{s:?}")),
133 Ok(yog_data::Value::Lang(key)) => Ok(tr(&key)),
134 Ok(yog_data::Value::Res(r)) => {
135 RESOURCE_REFS.with(|refs| refs.borrow().text(&r).map(|(k, _)| tr(k)))
136 }
137 Ok(yog_data::Value::Ref(r))
138 if matches!(
139 r.kind.as_str(),
140 yog_ids::kind::ITEM | yog_ids::kind::BLOCK | yog_ids::kind::ENTITY
141 ) =>
142 {
143 let id = r.id.split(STATE_OPEN).next().unwrap_or(&r.id);
145 Ok(tr(&yog_ids::lang_key(&r.kind, id)))
146 }
147 Ok(other) => Err(format!("{} isn't a text", other.kind_name())),
148 Err(_) if !value.contains(':') => Err(format!(
149 "write a translation key as `@lang:{}`, a .yres text as `@Set.name`, a literal `@` as `@@`",
150 value[1..].trim()
151 )),
152 Err(e) => Err(e),
153 }
154}
155
156const STATE_OPEN: char = '[';
158
159pub(crate) fn text_attr(el: &Element, key: &str, value: &str) -> Result<String, String> {
161 text_expr(value).map_err(|e| format!("line {}: <{}> {key}=\"{value}\": {e}", el.line, el.name))
162}
163
164thread_local! {
165 static RESOURCE_REFS: std::cell::RefCell<crate::yres::Refs> = Default::default();
167}
168
169pub fn set_resource_refs(refs: &crate::yres::Refs) {
172 RESOURCE_REFS.with(|r| *r.borrow_mut() = refs.clone());
173}
174
175pub fn const_name(value: &str) -> String {
177 let mut out = String::new();
178 let mut prev_lower = false;
179 for c in value.chars() {
180 if c.is_ascii_alphanumeric() {
181 if c.is_ascii_uppercase() && prev_lower {
182 out.push('_');
183 }
184 prev_lower = c.is_ascii_lowercase() || c.is_ascii_digit();
185 out.push(c.to_ascii_uppercase());
186 } else {
187 if !out.ends_with('_') {
188 out.push('_');
189 }
190 prev_lower = false;
191 }
192 }
193 let out = out.trim_matches('_').to_string();
194 if out.is_empty() || out.starts_with(|c: char| c.is_ascii_digit()) {
195 format!("_{out}")
196 } else {
197 out
198 }
199}
200
201#[derive(Debug, Default)]
203pub(crate) struct Names {
204 list: Vec<String>,
205 seen: HashSet<String>,
206}
207
208impl Names {
209 pub(crate) fn push(&mut self, name: &str) {
210 if !self.seen.contains(name) {
211 self.seen.insert(name.to_string());
212 self.list.push(name.to_string());
213 }
214 }
215
216 pub(crate) fn as_slice(&self) -> &[String] {
217 &self.list
218 }
219
220 pub(crate) fn into_vec(self) -> Vec<String> {
221 self.list
222 }
223}
224
225pub(crate) fn str_consts(values: &[String]) -> String {
228 let mut out = String::new();
229 let mut used: HashSet<String> = HashSet::new();
230 for v in values {
231 let mut name = const_name(v);
232 while used.contains(&name) {
233 name.push('_');
234 }
235 let _ = writeln!(out, " pub const {name}: &str = {v:?};");
236 used.insert(name);
237 }
238 out
239}
240
241pub(crate) fn is_image_value(v: &str) -> bool {
244 let v = v.trim();
245 match v.strip_prefix('@') {
246 Some(name) => RESOURCE_REFS.with(|r| r.borrow().images.contains_key(name)),
247 None => !v.starts_with('#') && v.contains(':'),
248 }
249}
250
251pub(crate) fn image_source(
254 el: &Element,
255 attr: &str,
256 source: &str,
257 mod_id: Option<&str>,
258) -> Result<String, String> {
259 let line = el.line;
260 let kind = el.name.as_str();
261 let mod_id = mod_id.map(str::to_owned);
262 let resolved;
263 let source = match source.strip_prefix('@') {
264 Some(name) => {
265 resolved = RESOURCE_REFS
266 .with(|r| r.borrow().images.get(name).cloned())
267 .ok_or_else(|| format!("line {line}: <{kind}> {attr}=\"{source}\": no image `{name}` in resources/*.yres"))?;
268 resolved.as_str()
269 }
270 None => source,
271 };
272 let parsed = ImageSource::parse(source).ok_or_else(|| {
273 format!(
274 "line {line}: <{kind}> {attr}=\"{source}\": expected item:<id>, block:<id>, \
275 sprite:<name>, mod:<path> or <namespace>:<path>"
276 )
277 })?;
278 let source = match parsed {
279 ImageSource::ModPath(path) => match &mod_id {
280 Some(id) => yog_gfx::image::texture_id(id, &path),
281 None => {
282 return Err(format!(
283 "line {line}: <{kind}> {attr}=\"{source}\": mod: needs the mod id (no yog.toml found)"
284 ));
285 }
286 },
287 ImageSource::Sprite(name)
288 if !name.contains(':') && yog_gfx::image::sprite(&name).is_none() =>
289 {
290 let names: Vec<&str> = yog_gfx::image::SPRITES
291 .iter()
292 .map(|s| s.name.as_str())
293 .collect();
294 return Err(format!(
295 "line {line}: <{kind}> {attr}=\"{source}\": unknown sprite (known: {})",
296 names.join(", ")
297 ));
298 }
299 ImageSource::Texture(_) => {
300 if let Some(alias) = yog_gfx::image::suggest_alias(source) {
301 return Err(format!(
302 "line {line}: <{kind}> {attr}=\"{source}\" is a raw resource path, which \
303 changes between Minecraft versions — write {attr}=\"{alias}\""
304 ));
305 }
306 source.to_string()
307 }
308 _ => source.to_string(),
309 };
310 Ok(source)
311}
312
313pub(super) fn font_attr<'a>(el: &Element, v: &'a str) -> Result<&'a str, String> {
316 let font = v.trim();
317 match yog_ids::split(font) {
318 (yog_ids::ns::MINECRAFT, name) if !font.is_empty() => err(
319 el,
320 format!(
321 "font=\"{font}\": the game's fonts are named by Yog's alias, {}",
322 yog_ids::font_alias(name)
323 ),
324 ),
325 _ => Ok(font),
326 }
327}