Skip to main content

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