Skip to main content

yog_cli/yui/
values.rs

1//! The values of `.yui` attributes: numbers, flags, colors, texts, image
2//! sources, names — read and turned into Rust.
3
4use std::collections::HashSet;
5use std::fmt::Write as _;
6
7use yog_gfx::image::ImageSource;
8
9use super::{Element, err};
10
11/// A number attribute.
12pub(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
19/// A yes/no attribute: `true` or `false`.
20pub(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
31/// `#RRGGBB`, `#AARRGGBB` or `0xAARRGGBB` → `0xAARRGGBB` (`Color::parse`;
32/// a file's color is written with its `#` or `0x`, not as a decimal).
33pub fn color(value: &str) -> Option<u32> {
34    yog_ui::attr::written_color(value).map(|c| c.0)
35}
36
37/// What a color written in hex starts with: `#RRGGBB`.
38pub const HEX_MARK: &str = yog_ui::attr::HEX_MARKS[0];
39
40/// A color attribute: a literal (see [`color`]) or `@<Set>.<name>`, a color
41/// of `resources/<Set>.yres` (see [`set_resource_refs`]).
42pub(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
59/// Whether `@<Set>.<name>` names a text of the mod's `.yres` resources.
60pub(super) fn is_resource_text(reference: &str) -> bool {
61    RESOURCE_REFS.with(|r| r.borrow().texts.contains_key(reference.trim()))
62}
63
64/// The color `@<Set>.<name>` names in the mod's `.yres` resources.
65pub(super) fn resource_color(reference: &str) -> Option<u32> {
66    RESOURCE_REFS.with(|r| r.borrow().colors.get(reference.trim()).copied())
67}
68
69/// `MinHeight` → `min_height`.
70pub(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
85/// `value`, one of `options`.
86pub(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
106/// `stat_bar` / `my-mod` → `StatBar` / `MyMod`.
107pub 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
119/// The Rust expression for a text attribute, its `@` forms as everywhere
120/// in Yog Data (`yog_data::typed`): `"@Strings.title"` is a text of
121/// `resources/Strings.yres` (see [`set_resource_refs`]), `"@lang:gui.done"`
122/// any translation key, `"@item:minecraft:stick"` (`@block:`, `@entity:`)
123/// that content's name — each looked up when the tree is built
124/// (`::yog_api::tr`, so it follows the game's language); `"@@..."` is a
125/// literal text starting with `@`; anything else is the literal text.
126pub 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            // A block's state (`[facing=north]`) isn't part of its name.
144            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
156/// Where a block id's state begins: `minecraft:furnace[facing=north]`.
157const STATE_OPEN: char = '[';
158
159/// [`text_expr`] for attribute `key` of `el`, a problem saying where.
160pub(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    /// The mod's `.yres` resources, for `@<Set>.<name>` in attributes.
166    static RESOURCE_REFS: std::cell::RefCell<crate::yres::Refs> = Default::default();
167}
168
169/// Makes the mod's compiled `.yres` resources visible to the generator:
170/// `text="@Strings.title"` and `source="@Icons.logo"` resolve through them.
171pub fn set_resource_refs(refs: &crate::yres::Refs) {
172    RESOURCE_REFS.with(|r| *r.borrow_mut() = refs.clone());
173}
174
175/// A Rust constant name for an event/widget id: `mymod:btn-play` → `MYMOD_BTN_PLAY`.
176pub 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/// Names in first-seen order, each once.
202#[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
225/// `pub const NAME: &str = "value";` lines for `values`, the names from
226/// [`const_name`] (a `_` appended to repeats).
227pub(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
241/// Whether a `bg` value is an image (`sprite:panel`, `mymod:gui/frame`,
242/// an `@Set.name` image) rather than a color (`#RRGGBB`, `@Colors.name`).
243pub(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
251/// An image source attribute (`<Image source>`, `bg="sprite:..."`): checks
252/// it and resolves `@Set.name` and `mod:` → the source the host draws.
253pub(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
313/// A `font` attribute: a Yog font, or a game font by Yog's alias — never
314/// the game's own id.
315pub(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}