Skip to main content

nappgui_macros/
lib.rs

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>, // UID -> ID
43    locates: HashSet<String>,
44    resource: Vec<HashMap<String, ResourceData>>, // Locale -> ResourceData
45}
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            // support comment start with /* end with */
96            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            // support comment start with //
110            if line.starts_with("//") {
111                continue;
112            }
113            // support empty line
114            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            // support message with ""
122            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            // folder name <-> locale
166            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    // init public uid definition
236    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}