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