1use std::collections::BTreeMap;
5use std::path::{Path, PathBuf};
6
7use super::names::{name_problem, valid_locale, valid_set_name};
8use super::{Entry, Kind, NEUTRAL_LOCALE, find, parse};
9use crate::diagnostics::Diagnostic;
10use crate::shown_path;
11use crate::yres::problem;
12
13#[derive(Debug, Clone)]
15pub struct ResFile {
16 pub path: PathBuf,
17 pub entries: Vec<Entry>,
18}
19
20#[derive(Debug, Clone)]
22pub struct Set {
23 pub name: String,
24 pub neutral: ResFile,
25 pub cultures: BTreeMap<String, ResFile>,
26}
27
28struct Read {
30 set: String,
31 locale: Option<String>,
32 file: ResFile,
33}
34
35pub(super) fn load(root: &Path, errors: &mut Vec<Diagnostic>) -> BTreeMap<String, Set> {
37 let mut sets: BTreeMap<String, Set> = BTreeMap::new();
38 let mut cultures: Vec<Read> = Vec::new();
39 for path in find(root) {
40 let Some(read) = read_file(root, path, errors) else {
41 continue;
42 };
43 match read.locale {
44 None => add_neutral(root, &mut sets, read, errors),
45 Some(_) => cultures.push(read),
46 }
47 }
48 for read in cultures {
49 add_culture(root, &mut sets, read, errors);
50 }
51 sets
52}
53
54fn read_file(root: &Path, path: PathBuf, errors: &mut Vec<Diagnostic>) -> Option<Read> {
57 let shown = shown_path(root, &path);
58 let file_name = path
59 .file_name()
60 .unwrap_or_default()
61 .to_string_lossy()
62 .into_owned();
63 let (set, locale) = yog_formats::yres::split_file_name(&file_name)?;
64 if let Err(e) = check_name(set, locale) {
65 errors.push(problem(e.to_string()).in_file(&shown));
66 return None;
67 }
68 let text = match crate::files::read_source(&path) {
69 Ok(text) => text,
70 Err(e) => {
71 errors.push(problem(e).in_file(&shown));
72 return None;
73 }
74 };
75 let entries = match parse(&text) {
76 Ok(entries) => entries,
77 Err(found) => {
78 errors.extend(found.0.into_iter().map(|l| l.in_file(&shown)));
79 return None;
80 }
81 };
82 for e in &entries {
83 if let Some(found) = name_problem(&e.name) {
84 errors.push(problem(found).in_file(&shown).at_line(e.line));
85 }
86 }
87 Some(Read {
88 set: set.to_string(),
89 locale: locale.map(str::to_string),
90 file: ResFile { path, entries },
91 })
92}
93
94fn check_name(set: &str, locale: Option<&str>) -> Result<(), String> {
96 if !valid_set_name(set) {
97 return Err(format!(
98 "`{set}` isn't a valid resource set name — use an identifier like Strings or Icons"
99 ));
100 }
101 match locale {
102 Some(NEUTRAL_LOCALE) => Err(format!(
103 "the neutral file {set}.yres is already {NEUTRAL_LOCALE} — put these texts there"
104 )),
105 Some(l) if !valid_locale(l) => Err(format!(
106 "`{l}` isn't a Minecraft locale code (lower case, like ru_ru or pt_br)"
107 )),
108 _ => Ok(()),
109 }
110}
111
112fn add_neutral(
114 root: &Path,
115 sets: &mut BTreeMap<String, Set>,
116 read: Read,
117 errors: &mut Vec<Diagnostic>,
118) {
119 if let Some(other) = sets.get(&read.set) {
120 let (shown, other) = (
121 shown_path(root, &read.file.path),
122 shown_path(root, &other.neutral.path),
123 );
124 errors.push(
125 problem(format!(
126 "resource set {} is also defined by {other}",
127 read.set
128 ))
129 .in_file(&shown),
130 );
131 return;
132 }
133 sets.insert(
134 read.set.clone(),
135 Set {
136 name: read.set,
137 neutral: read.file,
138 cultures: BTreeMap::new(),
139 },
140 );
141}
142
143fn add_culture(
145 root: &Path,
146 sets: &mut BTreeMap<String, Set>,
147 read: Read,
148 errors: &mut Vec<Diagnostic>,
149) {
150 let Read { set, locale, file } = read;
151 let locale = locale.unwrap_or_default();
152 let shown = shown_path(root, &file.path);
153 let Some(target) = sets.get_mut(&set) else {
154 errors.push(
155 problem(format!("no neutral {set}.yres for this culture variant")).in_file(&shown),
156 );
157 return;
158 };
159 if let Some(other) = target.cultures.get(&locale) {
160 errors.push(
161 problem(format!(
162 "{set} {locale} is also defined by {}",
163 shown_path(root, &other.path)
164 ))
165 .in_file(&shown),
166 );
167 return;
168 }
169 for e in file
170 .entries
171 .iter()
172 .filter(|e| e.kind != Kind::String || e.key.is_some())
173 {
174 errors.push(
175 problem(format!(
176 "{:?}: culture variants hold texts only (no type other than string, no `key`)",
177 e.name
178 ))
179 .in_file(&shown)
180 .at_line(e.line),
181 );
182 }
183 target.cultures.insert(locale, file);
184}