1use std::{
2 collections::{HashMap, HashSet},
3 path::{Path, PathBuf},
4};
5
6use proc_macro::TokenStream;
7
8#[proc_macro]
9pub fn include_resource(input: TokenStream) -> TokenStream {
10 let input = input.to_string();
11 let input = input.trim();
12
13 let mut path = Path::new(&input).to_owned();
14 if !path.is_absolute() {
15 let local_file = proc_macro::Span::call_site()
16 .local_file()
17 .expect("Unable to get local file!");
18 let local_file_parent = local_file
19 .parent()
20 .expect("Unable to get the parent local file!");
21 path = local_file_parent.join(input);
22 }
23
24 let resource = read_data(path);
25 let code = generate_code(&resource);
26
27 code.parse().expect("Unable to parse resources!")
28}
29
30#[derive(Debug)]
31enum ResourceData {
32 Message(String),
33 Bytes(PathBuf),
34 File(PathBuf),
35}
36
37#[derive(Debug)]
38struct Resource {
39 rid: String,
40 size: usize,
41 uids_ordered: Vec<String>,
42 uids: HashMap<String, usize>, locates: HashSet<String>,
44 resource: Vec<HashMap<String, ResourceData>>, }
46
47impl Resource {
48 fn new(rid: &str) -> Self {
49 Self {
50 rid: rid.to_owned(),
51 size: 0,
52 uids_ordered: Vec::new(),
53 uids: HashMap::new(),
54 locates: HashSet::new(),
55 resource: Vec::new(),
56 }
57 }
58
59 fn get_sid(&self, uid: &str) -> Option<String> {
60 self.uids
61 .get(uid)
62 .map(|id| format!("N23R3C75::{}::{}", self.rid, id))
63 }
64
65 fn get_id(&self, uid: &str) -> Option<usize> {
66 self.uids.get(uid).copied()
67 }
68
69 fn push(&mut self, uid: &str, locale: &str, data: ResourceData) {
70 let id = if !self.uids.contains_key(uid) {
71 let id = self.size;
72 self.uids.insert(uid.to_owned(), id);
73 self.uids_ordered.push(uid.to_owned());
74 self.resource.push(HashMap::new());
75 self.size += 1;
76 id
77 } else {
78 *self
79 .uids
80 .get(uid)
81 .expect("Unable to get uid when add data to Resource!")
82 };
83
84 self.locates.insert(locale.to_uppercase());
85 self.resource
86 .get_mut(id)
87 .expect("Unable to get resource!")
88 .insert(locale.to_uppercase(), data);
89 }
90
91 fn push_message(&mut self, locale: &str, message: &str) {
92 let mut comment = false;
93 for line in message.lines() {
94 let line = line.trim();
95 if comment {
97 if line.ends_with("*/") {
98 comment = false;
99 }
100 continue;
101 }
102 if line.starts_with("/*") {
103 comment = true;
104 if line.trim().ends_with("*/") {
105 comment = false;
106 }
107 continue;
108 }
109 if line.starts_with("//") {
111 continue;
112 }
113 if line.len() == 0 {
115 continue;
116 }
117 let line = line
118 .split_once(char::is_whitespace)
119 .expect(&format!("Unable to get id and message in line {}!", line));
120 let uid = line.0.to_uppercase();
121 let message = line.1.trim().trim_matches('"').to_string();
123 self.push(&uid, locale, ResourceData::Message(message));
124 }
125 }
126
127 fn push_file(&mut self, locale: &str, file: &Path) {
128 assert!(file.is_file());
129
130 let name = file.file_name().unwrap().to_string_lossy().to_string();
131 let extension = file.extension().unwrap().to_string_lossy().to_string();
132 match extension.as_ref() {
133 "msg" => {
134 let text = std::fs::read_to_string(file).expect("Unable to read messages");
135 self.push_message(&locale, &text);
136 }
137 "png" | "jpg" | "gif" | "bmp" => {
138 let uid = name.replace(".", "_").to_uppercase();
139 self.push(&uid, &locale, ResourceData::Bytes(file.to_owned()));
140 }
141 _ => {
142 let uid = name.replace(".", "_").to_uppercase();
143 self.push(&uid, &locale, ResourceData::File(file.to_owned()));
144 }
145 }
146 }
147}
148
149const DEFAULT: &str = "DEFAULT";
150
151fn read_data<P>(dir: P) -> Resource
152where
153 P: AsRef<Path>,
154{
155 let dir = Path::new(dir.as_ref());
156 let rid = dir
157 .file_name()
158 .expect("Unable to get resource id from folder!");
159 let mut resources = Resource::new(rid.to_string_lossy().as_ref());
160
161 let items = std::fs::read_dir(dir).expect("Unable to read items in resource folder!");
162 for item in items {
163 let path = item.unwrap().path();
164 if path.is_dir() {
165 let locale = path
167 .file_name()
168 .expect("Unable to get locale from folder!")
169 .to_string_lossy();
170 let inner_items = std::fs::read_dir(&path).unwrap();
171 for inner_item in inner_items {
172 let inner_path = inner_item.unwrap().path();
173 if inner_path.is_dir() {
174 continue;
175 }
176 resources.push_file(&locale, &inner_path);
177 }
178 } else {
179 resources.push_file(DEFAULT, &path);
180 }
181 }
182 resources
183}
184
185fn generate_static_object(uid: &str, locale: &str, data: &ResourceData) -> String {
186 let locale = if locale == DEFAULT {
187 "".to_owned()
188 } else {
189 format!("{}_", locale)
190 };
191 match data {
192 ResourceData::Message(message) => {
193 format!(
194 "static {}_{}TEXT: &'static str = \"{}\";",
195 uid, locale, message
196 )
197 }
198 ResourceData::Bytes(path) | ResourceData::File(path) => {
199 format!(
200 "static {}_{}DATA: &'static [u8] = include_bytes!(\"{}\");",
201 uid,
202 locale,
203 std::path::absolute(path)
204 .unwrap()
205 .as_os_str()
206 .to_string_lossy()
207 .replace("\\", "/")
208 )
209 }
210 }
211}
212
213fn generate_add_resource(uid: &str, locale: &str, data: &ResourceData) -> String {
214 let locale = if locale == DEFAULT {
215 "".to_owned()
216 } else {
217 format!("{}_", locale)
218 };
219 match data {
220 ResourceData::Message(_) => {
221 format!("respack.add_message({}_{}TEXT);", uid, locale)
222 }
223 ResourceData::Bytes(_) => {
224 format!("respack.add_bytes({}_{}DATA);", uid, locale)
225 }
226 ResourceData::File(_) => {
227 format!("respack.add_file({}_{}DATA);", uid, locale)
228 }
229 }
230}
231
232fn generate_code(resource: &Resource) -> String {
233 let mut code: Vec<String> = Vec::new();
234
235 for uid in resource.uids_ordered.iter() {
237 code.push(format!(
238 "pub static {}: &str = \"{}\";",
239 uid,
240 resource.get_sid(uid).unwrap(),
241 ));
242
243 let id = resource.get_id(uid).unwrap();
244 for (local, data) in resource.resource[id].iter() {
245 code.push(generate_static_object(uid, local, data));
246 }
247 }
248
249 code.push(format!("pub unsafe extern \"C\" fn {}_respack(locale: *const std::ffi::c_char) -> nappgui::core::ResPackPtr {{", resource.rid));
250 code.push("#[allow(unused)]".to_owned());
251 code.push(
252 "let locale = unsafe { std::ffi::CStr::from_ptr(locale).to_str().unwrap() };".to_owned(),
253 );
254 code.push(format!(
255 "let mut respack = nappgui::core::ResPack::new_embedded(\"{}\");",
256 resource.rid
257 ));
258
259 for locale in resource.locates.iter() {
260 if locale == DEFAULT {
261 continue;
262 }
263 code.push(format!("if locale == \"{}\" {{", locale));
264 for uid in resource.uids_ordered.iter() {
265 let id = resource.get_id(uid).unwrap();
266 let locale = if resource.resource[id].contains_key(locale) {
267 locale
268 } else {
269 DEFAULT
270 };
271 let data = &resource.resource[id][locale];
272 code.push(generate_add_resource(uid, locale, data))
273 }
274 code.push("return respack.as_ptr()".to_owned());
275 code.push("}".to_owned());
276 }
277
278 for uid in resource.uids_ordered.iter() {
279 let id = resource.get_id(uid).unwrap();
280 let data = &resource.resource[id][DEFAULT];
281 code.push(generate_add_resource(uid, DEFAULT, data));
282 }
283
284 code.push("respack.as_ptr()".to_owned());
285 code.push("}".to_owned());
286
287 let code = code.join("\n");
288 code
289}