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};
7use yog_data::Diagnostic;
8
9/// Where generated screen code goes, under the project root.
10pub const GENERATED_DIR: &str = ".yog-build/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::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/// `.yog-build/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 `.yog-build/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| d.in_file(&shown).or_code(yog_ids::build_problem::SCREEN);
62    let src = std::fs::read_to_string(yui).map_err(|e| screen(e.to_string().into()))?;
63    let name = include_name(root, yui);
64    let mod_id = std::fs::read_to_string(root.join(crate::manifest::MANIFEST))
65        .ok()
66        .and_then(|t| crate::manifest::parse_yog_toml(&t).ok())
67        .map(|t| t.id);
68    // The mod's recipe layouts, for the screen's <RecipeArea>s.
69    let layouts: Vec<yog_formats::book::layout::RecipeLayout> = if src.contains(RECIPE_AREA_TAG) {
70        crate::book_page::find_layouts(
71            root,
72            mod_id.as_deref().unwrap_or(yog_ids::ns::MINECRAFT),
73            &mut Vec::new(),
74        )
75        .into_iter()
76        .map(|(_, l)| l)
77        .collect()
78    } else {
79        Vec::new()
80    };
81    let (code, warnings) =
82        generate_with(&src, &name, mod_id.as_deref(), &layouts).map_err(screen)?;
83    let out = generated_path(root, &name);
84    crate::write_if_changed(&out, &code)?;
85    Ok((out, warnings.into_iter().map(screen).collect()))
86}
87
88/// What opens a `<RecipeArea>`: a screen with one needs the mod's layouts.
89const RECIPE_AREA_TAG: &str = yog_ids::concatcp!("<", yog_ids::yui::RECIPE_AREA);
90
91/// Whether `path`'s root is a `<Component book_page="…">`.
92pub fn is_book_page_file(path: &Path) -> bool {
93    std::fs::read_to_string(path)
94        .ok()
95        .and_then(|s| parse_xml(&s).ok())
96        .is_some_and(|r| crate::book_page::is_book_ui(&r))
97}
98
99/// Every `.yui` file under `dir`, recursively.
100pub fn find_yui_files(dir: &Path) -> Vec<PathBuf> {
101    crate::files::with_ext(dir, yog_ids::Format::Ui.ext())
102}
103
104/// What [`generate_all`] did: how many screens it read, what to warn of.
105pub struct Generated {
106    /// How many screens it read.
107    pub screens: usize,
108    /// What to warn of.
109    pub warnings: Vec<Diagnostic>,
110}
111
112/// `yog build`'s step: regenerates every `src/**/*.yui`'s `.g.rs` and drops
113/// generated files whose `.yui` is gone.
114pub fn generate_all(root: &Path) -> Result<Generated, Diagnostic> {
115    let files = find_yui_files(&root.join(yog_ids::project::SRC));
116    let mut wanted = Vec::new();
117    let mut warnings = Vec::new();
118    for file in &files {
119        // A kind of book page is the book's (`crate::book_page`), not a widget.
120        if is_book_page_file(file) {
121            continue;
122        }
123        let (out, w) = generate_file(root, file)?;
124        wanted.push(out);
125        warnings.extend(w);
126    }
127    // Components without a code-behind, for `ui_components!()`.
128    let (index, warning) = crate::yui_component::write_index(root)?;
129    wanted.push(index);
130    warnings.extend(warning.map(|w| Diagnostic::warning(yog_ids::build_problem::SCREEN, w)));
131    // `.yres` accessors live there too (`crate::yres` cleans those up).
132    let res_index = root.join(crate::yres::GENERATED_FILE);
133    let res_dir = root.join(crate::yres::GENERATED_DIR);
134    // So do `content/` blocks and items (`crate::content`).
135    let content_file = root.join(crate::content::GENERATED_FILE);
136    let content_parts = content_file
137        .parent()
138        .map(|d| d.join(crate::content::GENERATED_PARTS_DIR))
139        .unwrap_or_default();
140    for stale in crate::files::with_ext(&root.join(GENERATED_DIR), RS) {
141        if stale.to_string_lossy().ends_with(GENERATED_EXT)
142            && !wanted.contains(&stale)
143            && stale != res_index
144            && stale != content_file
145            && !stale.starts_with(&content_parts)
146            && !stale.starts_with(&res_dir)
147        {
148            crate::files::remove(&stale);
149        }
150    }
151    Ok(Generated {
152        screens: files.len(),
153        warnings,
154    })
155}