Skip to main content

yog_cli/yui/
files.rs

1//! Where `.yui` files and their generated code are: finding them, naming
2//! them, generating every one of a project.
3
4use std::path::{Path, PathBuf};
5
6use super::{generate_with, parse_xml};
7
8/// Where generated screen code goes, under the project root.
9pub const GENERATED_DIR: &str = ".yog-build/generated";
10
11/// `path` from the current folder when it's relative.
12fn absolute(path: &Path) -> PathBuf {
13    if path.is_absolute() {
14        path.to_path_buf()
15    } else {
16        std::env::current_dir().unwrap_or_default().join(path)
17    }
18}
19
20/// The project root (the folder with `yog.toml`) a `.yui` file belongs to.
21pub fn project_root(yui: &Path) -> Option<PathBuf> {
22    absolute(yui)
23        .ancestors()
24        .skip(1)
25        .find(|d| d.join(crate::manifest::MANIFEST).is_file())
26        .map(Path::to_path_buf)
27}
28
29/// `foo.yui`'s name for `ui_include!`: its path under `src/` without the
30/// extension, `/`-separated (`mod_list`, `screens/options`).
31pub fn include_name(root: &Path, yui: &Path) -> String {
32    let abs = absolute(yui);
33    let rel = abs
34        .strip_prefix(root.join(yog_ids::project::SRC))
35        .or_else(|_| abs.strip_prefix(root))
36        .unwrap_or(&abs)
37        .with_extension("");
38    rel.components()
39        .map(|c| c.as_os_str().to_string_lossy().into_owned())
40        .collect::<Vec<_>>()
41        .join(yog_ids::sep::PATH)
42}
43
44/// Where `src/<name>.yui`'s generated code goes:
45/// `.yog-build/generated/<name>.g.rs`.
46pub fn generated_path(root: &Path, name: &str) -> PathBuf {
47    root.join(GENERATED_DIR)
48        .join(format!("{name}{GENERATED_EXT}"))
49}
50
51/// Rust files, and what a generated one's name ends with.
52const RS: &str = "rs";
53const GENERATED_EXT: &str = ".g.rs";
54
55/// Generates `.yog-build/generated/<name>.g.rs` for one `.yui` file of the
56/// project at `root` — only rewritten when the output changed, so cargo
57/// doesn't rebuild for nothing. Its path, and what to warn of.
58pub fn generate_file(root: &Path, yui: &Path) -> Result<(PathBuf, Vec<String>), String> {
59    let src = std::fs::read_to_string(yui).map_err(|e| format!("{}: {e}", yui.display()))?;
60    let name = include_name(root, yui);
61    let mod_id = std::fs::read_to_string(root.join(crate::manifest::MANIFEST))
62        .ok()
63        .and_then(|t| crate::manifest::parse_yog_toml(&t).ok())
64        .map(|t| t.id);
65    // The mod's recipe layouts, for the screen's <RecipeArea>s.
66    let layouts: Vec<yog_formats::book::layout::RecipeLayout> = if src.contains(RECIPE_AREA_TAG) {
67        crate::book_page::find_layouts(
68            root,
69            mod_id.as_deref().unwrap_or(yog_ids::ns::MINECRAFT),
70            &mut Vec::new(),
71        )
72        .into_iter()
73        .map(|(_, l)| l)
74        .collect()
75    } else {
76        Vec::new()
77    };
78    let (code, warnings) = generate_with(&src, &name, mod_id.as_deref(), &layouts)
79        .map_err(|e| format!("{}: {e}", yui.display()))?;
80    let out = generated_path(root, &name);
81    crate::write_if_changed(&out, &code)?;
82    Ok((
83        out,
84        warnings
85            .into_iter()
86            .map(|w| format!("{}: {w}", yui.display()))
87            .collect(),
88    ))
89}
90
91/// What opens a `<RecipeArea>`: a screen with one needs the mod's layouts.
92const RECIPE_AREA_TAG: &str = yog_ids::concatcp!("<", yog_ids::yui::RECIPE_AREA);
93
94/// Whether `path`'s root is a `<Component book_page="…">`.
95pub fn is_book_page_file(path: &Path) -> bool {
96    std::fs::read_to_string(path)
97        .ok()
98        .and_then(|s| parse_xml(&s).ok())
99        .is_some_and(|r| crate::book_page::is_book_ui(&r))
100}
101
102/// Every `.yui` file under `dir`, recursively.
103pub fn find_yui_files(dir: &Path) -> Vec<PathBuf> {
104    crate::files::with_ext(dir, yog_ids::Format::Ui.ext())
105}
106
107/// What [`generate_all`] did: how many screens it read, what to warn of.
108pub struct Generated {
109    pub screens: usize,
110    pub warnings: Vec<String>,
111}
112
113/// `yog build`'s step: regenerates every `src/**/*.yui`'s `.g.rs` and drops
114/// generated files whose `.yui` is gone.
115pub fn generate_all(root: &Path) -> Result<Generated, String> {
116    let files = find_yui_files(&root.join(yog_ids::project::SRC));
117    let mut wanted = Vec::new();
118    let mut warnings = Vec::new();
119    for file in &files {
120        // A kind of book page is the book's (`crate::book_page`), not a widget.
121        if is_book_page_file(file) {
122            continue;
123        }
124        let (out, w) = generate_file(root, file)?;
125        wanted.push(out);
126        warnings.extend(w);
127    }
128    // Components without a code-behind, for `ui_components!()`.
129    let (index, warning) = crate::yui_component::write_index(root)?;
130    wanted.push(index);
131    warnings.extend(warning);
132    // `.yres` accessors live there too (`crate::yres` cleans those up).
133    let res_index = root.join(crate::yres::GENERATED_FILE);
134    let res_dir = root.join(crate::yres::GENERATED_DIR);
135    // So do `content/` blocks and items (`crate::content`).
136    let content_file = root.join(crate::content::GENERATED_FILE);
137    let content_parts = content_file
138        .parent()
139        .map(|d| d.join(crate::content::GENERATED_PARTS_DIR))
140        .unwrap_or_default();
141    for stale in crate::files::with_ext(&root.join(GENERATED_DIR), RS) {
142        if stale.to_string_lossy().ends_with(GENERATED_EXT)
143            && !wanted.contains(&stale)
144            && stale != res_index
145            && stale != content_file
146            && !stale.starts_with(&content_parts)
147            && !stale.starts_with(&res_dir)
148        {
149            let _ = std::fs::remove_file(&stale);
150        }
151    }
152    Ok(Generated {
153        screens: files.len(),
154        warnings,
155    })
156}