1use std::borrow::Cow;
2use std::path::Path;
3use concat_string::concat_string;
6use itertools::Itertools;
7use thiserror::Error;
8
9#[cfg(feature = "vfs")]
10use vfs::VfsPath;
11
12#[cfg(not(feature = "vfs"))]
13type FeatPath = std::path::Path;
14#[cfg(feature = "vfs")]
15type FeatPath = vfs::VfsPath;
16
17fn read_to_string(input_file: &FeatPath) -> Result<String> {
18 #[cfg(not(feature = "vfs"))] {
19 return read_to_string_std(input_file);
20 }
21 #[cfg(feature = "vfs")] {
22 let mut result = String::new();
23 input_file.open_file()?.read_to_string(&mut result)
24 .map_err(|err| Error::IOError(err, input_file.as_str().into()))?;
25 return Ok(result);
26 }
27}
28
29fn read_to_string_std(input_file: &Path) -> Result<String> {
30 return std::fs::read_to_string(input_file)
31 .map_err(|err| Error::IOError(err, input_file.to_path_buf()));
32}
33
34#[derive(Error, Debug)]
35pub enum Error {
36 #[error("Invalid macro `{}` on line {}", .0, .1)]
37 InvalidMacro(String, usize),
38 #[error("Found extra parameters in #param macro on line {}", .0)]
39 ExtraParamsInParamMacro(usize),
40 #[error("Not enough parameters passed to template file")]
41 NotEnoughParameters,
42 #[error("Not enough parameters passed to function-like macro `{}`", .0)]
43 NotEnoughParametersMacro(String),
44 #[error("Invalid parameter name {} on line {}", .0, .1)]
45 InvalidParameterName(String, usize),
46 #[error("First parameter of #include should be a string on line {}", .0)]
47 FirstParamOfIncludeNotString(usize),
48 #[error("Unused parameters while expanding macro file")]
49 UnusedParameters,
50 #[error("IOError while reading {}: {}", .1.display(), .0)]
51 IOError(std::io::Error, std::path::PathBuf),
52 #[cfg(feature = "vfs")]
53 #[error("VfsError: {}", .0)]
54 VfsError(#[from] vfs::VfsError),
55}
56
57type Result<T> = std::result::Result<T, Error>;
58
59pub fn parse<'a>(
69 input_file: impl AsRef<Path>,
70 base_dir: impl AsRef<Path>,
71 parameters: impl Iterator<Item = &'a str>,
72) -> Result<String> {
73 return parse_cow(input_file, base_dir, parameters);
74}
75
76#[cfg(feature = "vfs")]
77pub fn parse_vfs<'a>(
78 input_file: impl Into<VfsPath>,
79 base_dir: impl Into<VfsPath>,
80 parameters: impl Iterator<Item = &'a str>
81) -> Result<String> {
82 return parse_vfs_cow(input_file.into(), base_dir.into(), parameters);
83}
84
85pub fn parse_cow<'a, Iter, C>(
88 input_file: impl AsRef<Path>,
89 base_dir: impl AsRef<Path>,
90 parameters: Iter
91) -> Result<String>
92 where
93 Iter: Iterator<Item = C>,
94 C: Into<Cow<'a, str>>
95{
96 let content = read_to_string_std(input_file.as_ref())?;
97
98 return parse_string_cow(&content, base_dir, parameters);
99}
100
101#[cfg(feature = "vfs")]
102pub fn parse_vfs_cow<'a, Iter, C>(
103 input_file: impl Into<VfsPath>,
104 base_dir: impl Into<VfsPath>,
105 parameters: Iter
106) -> Result<String>
107 where
108 Iter: Iterator<Item = C>,
109 C: Into<Cow<'a, str>>
110{
111 let content = read_to_string(&input_file.into())?;
112
113 return parse_string_vfs_cow(&content, base_dir, parameters);
114}
115
116pub fn parse_string<'a>(
138 input: &str,
139 base_dir: impl AsRef<Path>,
140 parameters: impl Iterator<Item = &'a str>
141) -> Result<String> {
142 parse_string_cow(input, base_dir, parameters)
143}
144
145#[cfg(feature = "vfs")]
146pub fn parse_string_vfs<'a>(
147 input: &str,
148 base_dir: impl Into<VfsPath>,
149 parameters: impl Iterator<Item = &'a str>
150) -> Result<String> {
151 parse_string_vfs_cow(input, base_dir, parameters)
152}
153
154pub fn parse_string_cow<'a, Iter, C>(
157 input: &str,
158 base_dir: impl AsRef<Path>,
159 parameters: Iter
160) -> Result<String>
161 where
162 Iter: Iterator<Item = C>,
163 C: Into<Cow<'a, str>>
164{
165 #[cfg(not(feature = "vfs"))]
166 let base_dir = base_dir.as_ref();
167 #[cfg(feature = "vfs")]
168 let base_dir = VfsPath::from(vfs::PhysicalFS::new(base_dir));
169 parse_string_cow_impl(input, &base_dir, &mut parameters.map(|v| v.into()))
170}
171
172#[cfg(feature = "vfs")]
173pub fn parse_string_vfs_cow<'a, Iter, C>(
174 input: &str,
175 base_dir: impl Into<VfsPath>,
176 parameters: Iter
177) -> Result<String>
178 where
179 Iter: Iterator<Item = C>,
180 C: Into<Cow<'a, str>>
181{
182 parse_string_cow_impl(input, &base_dir.into(), &mut parameters.map(|v| v.into()))
183}
184
185fn parse_string_cow_impl<'a>(
186 input: &str,
187 base_dir: &FeatPath,
188 parameters: &mut dyn Iterator<Item = Cow<'a, str>>
189) -> Result<String> {
190 let mut out = String::new();
191
192 let mut replacements: Vec<(String, Cow<str>)> = vec![];
193 let mut fn_replacements: Vec<(String, Vec<String>, String)> = vec![];
194 let mut cur_fn_replacement: Option<(String, Vec<String>, String)> = None;
195
196 let max_lines = input.chars()
197 .filter(|c| *c == '\n')
198 .count();
199
200 for (line_num, line) in input.lines().enumerate() {
201 if let Some(cur_fn_repl) = cur_fn_replacement {
202 if line.ends_with("\\") {
203 cur_fn_replacement = Some((cur_fn_repl.0, cur_fn_repl.1, cur_fn_repl.2 + &line[..line.len()-1]))
204 } else {
205 fn_replacements.push((cur_fn_repl.0, cur_fn_repl.1, cur_fn_repl.2 + line));
206 cur_fn_replacement = None;
207 }
208 continue;
209 }
210
211 let mut line_chars = line.chars().skip_while(char::is_ascii_whitespace);
212 let start_char = line_chars.by_ref().next();
213 match start_char {
214 Some('#') => {
215 let macro_name = line_chars.by_ref().take_while(|c| c.is_ascii_alphanumeric()).collect::<String>();
216 match macro_name.as_str() {
217 "define" => {
218 let mut is_last_bracket = false;
219 let name = line_chars.by_ref()
220 .skip_while(char::is_ascii_whitespace)
221 .take_while(|c| {
222 if *c == '(' {
223 is_last_bracket = true;
224 }
225 !c.is_ascii_whitespace() && *c != '('
226 })
227 .collect::<String>();
228
229 if is_last_bracket {
230 let params = line_chars.by_ref()
231 .take_while(|c| *c != ')')
232 .chunk_by(|c| *c == ',');
233 let params = params
234 .into_iter()
235 .filter(|(b, _)| !b)
236 .map(|(_, i)| i
237 .skip_while(char::is_ascii_whitespace)
238 .take_while(|c| !c.is_ascii_whitespace())
239 .collect::<String>())
240 .collect::<Vec<String>>();
241
242 let check_param_name = params.iter().find(|param| !param.chars().all(|c| c.is_alphanumeric() || c == '_'))
243 .or(params.iter().find(|param| param.len() == 0 || param.chars().next().unwrap().is_numeric()));
244 if let Some(param_name) = check_param_name {
245 return Err(Error::InvalidParameterName(param_name.clone(), line_num))
246 }
247
248 let replacement = line_chars.by_ref().collect::<String>();
249
250 if replacement.ends_with("\\") {
251 cur_fn_replacement = Some((name, params, replacement[..replacement.len()-1].to_string()));
252 } else {
253 fn_replacements.push((name, params, replacement));
254 }
255 } else {
256 let replacement = line_chars.collect::<Cow<str>>();
257 replacements.push((name, replacement))
258 }
259 }, "include" => {
260 let path = line_chars.by_ref()
261 .skip_while(char::is_ascii_whitespace)
262 .take_while(|c| !c.is_ascii_whitespace())
263 .collect::<String>();
264
265 if !(path.starts_with('"') && path.ends_with('"')) {
266 return Err(Error::FirstParamOfIncludeNotString(line_num));
267 }
268
269 let path = &path[1..path.len()-1];
270
271 let params = line_chars.by_ref()
272 .chunk_by(|c| *c == ',');
273 let mut params = params
274 .into_iter()
275 .filter(|(b, _)| !b)
276 .map(|(_, i)| Cow::Owned(i.collect::<String>()));
277
278 #[cfg(not(feature = "vfs"))]
279 let file_path = base_dir.join(path);
280
281 #[cfg(feature = "vfs")]
282 let file_path = base_dir.join(path)?;
283
284 let content = read_to_string(&file_path)?;
285
286 out += parse_string_cow_impl(&content, &base_dir, &mut params)?.as_str();
287 }, "param" => {
288 let param_name = line_chars.by_ref()
289 .skip_while(|c| c.is_ascii_whitespace())
290 .take_while(|c| !c.is_ascii_whitespace())
291 .collect::<String>();
292
293 if !line_chars.by_ref().all(|c| c.is_ascii_whitespace()) {
294 return Err(Error::ExtraParamsInParamMacro(line_num));
295 }
296
297 let Some(param_value) = parameters.next() else {
298 return Err(Error::NotEnoughParameters);
299 };
300
301 replacements.push((param_name, param_value.into()));
302 },
303 _ => return Err(Error::InvalidMacro(macro_name, line_num)),
304 }
305 },
306 Some('\\') if (line_chars.next() == Some('#')) => {
307 out += fn_replace(replace(&line.replacen("\\#", "#", 1), &replacements), &fn_replacements)?.as_ref();
308 if line_num != max_lines {
309 out += "\n";
310 }
311 },
312 _ => {
313 out += fn_replace(replace(line, &replacements), &fn_replacements)?.as_ref();
314 if line_num != max_lines {
315 out += "\n";
316 }
317 },
318 }
319 }
320
321 if parameters.count() != 0 {
322 return Err(Error::UnusedParameters);
323 }
324
325 return Ok(out);
326}
327
328fn is_ident(str: &str, start: usize, end: usize) -> bool {
329 (start == 0 || str.chars().nth(start - 1).map(|c| !(c.is_alphanumeric() || c == '_')).unwrap_or(true))
330 && str.chars().nth(end).map(|c| !(c.is_alphanumeric() || c == '_')).unwrap_or(true)
331}
332
333fn replace<'a>(line: &'a str, replacements: &Vec<(String, Cow<str>)>) -> Cow<'a, str> {
334 let mut out: Cow<str> = line.into();
335
336 for replacement in replacements {
337 out = replace_all(out, &replacement.0, replacement.1.as_ref(), is_ident);
338 }
339
340 return out;
341}
342
343fn fn_replace<'a>(line: Cow<'a, str>, replacements: &Vec<(String, Vec<String>, String)>) -> Result<Cow<'a, str>> {
344 let mut out: Cow<str> = line;
345
346 for replacement in replacements {
347 out = replace_all_fn(out, replacement.0.as_str(), replacement.2.as_str(), &replacement.1, is_ident)?;
348 }
349
350 return Ok(out);
351}
352
353fn replace_all<'a>(str: Cow<'a, str>, to_match: &str, replacement: &str, predicate: impl Fn(&str, usize, usize) -> bool) -> Cow<'a, str> {
354 let matches = str.match_indices(to_match).collect::<Vec<_>>();
355
356 let mut out: Option<Cow<str>> = None;
357 let mut end_idx = str.len();
358
359 for (idx, _) in matches.into_iter().rev() {
360 if predicate(str.as_ref(), idx, idx + to_match.len()) {
361 let following_str = &str[idx + to_match.len()..end_idx];
362 end_idx = idx;
363 out = out.map(|m| concat_string!(replacement, following_str, m.as_ref()).into())
364 .or(Some(concat_string!(replacement, following_str).into()));
365 }
366 }
367
368 if end_idx != 0 {
369 out = out.map(|m| concat_string!(&str[0..end_idx], m.as_ref()).into())
370 }
371
372 return out.unwrap_or(str);
373}
374
375fn replace_all_fn<'a>(
376 str: Cow<'a, str>,
377 name: &str,
378 replacement: &str,
379 param_names: &Vec<String>,
380 predicate: impl Fn(&str, usize, usize) -> bool
381) -> Result<Cow<'a, str>> {
382 let matches = str.match_indices(name).collect::<Vec<_>>();
383
384 let mut out: Option<Cow<str>> = None;
385 let mut end_idx = str.len();
386
387 for (idx, _) in matches.into_iter().rev() {
388 let mut iter = str.chars();
389 if iter.by_ref().nth(idx + name.len()) != Some('(') {
390 continue;
391 }
392
393 let mut open = 1;
394 let mut params = Vec::new();
395 let mut cur = String::new();
396 let mut param_len = 0;
397 for (i, c) in iter.enumerate() {
398 if c == '(' {
399 open += 1
400 } else if c == ')' {
401 open -= 1;
402 }
403
404 if open == 0 {
405 param_len = i;
406 if cur.len() != 0 {
407 params.push(cur);
408 }
409 break;
410 } else if open == 1 {
411 if c == ',' {
412 params.push(cur);
413 cur = String::new();
414 } else {
415 cur.push(c);
416 }
417 }
418 }
419
420 let to_replace_len = name.len() + 2 + param_len;
421
422 if !predicate(str.as_ref(), idx, idx + to_replace_len) {
423 continue;
424 }
425
426 if params.len() != param_names.len() {
427 return Err(Error::NotEnoughParametersMacro(name.to_string()));
428 }
429
430 let params = param_names.iter()
431 .zip(params.iter());
432
433 let mut replacement = Cow::Borrowed(replacement);
434 for param in params {
435 replacement = replace_all(replacement, param.0, param.1, is_ident);
436 }
437
438 let following_str = &str[idx + to_replace_len..end_idx];
439 end_idx = idx;
440 out = out.map(|m| concat_string!(replacement, following_str, m.as_ref()).into())
441 .or(Some(concat_string!(replacement, following_str).into()));
442 }
443
444 if end_idx != 0 {
445 out = out.map(|m| concat_string!(&str[0..end_idx], m.as_ref()).into());
446 }
447
448 return Ok(out.unwrap_or(str));
449}