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 ExtraParamsInMacro(usize, &'static str),
40 #[error("Not enough parameters passed to template file")]
41 NotEnoughParameters,
42 #[error("Unused parameters passed to template file")]
43 UnusedParameters,
44 #[error("Not enough parameters passed to function-like macro `{}`", .0)]
45 NotEnoughParametersMacro(String),
46 #[error("Too many parameters passed to function-like macro `{}`", .0)]
47 UnusedParametersMacro(String),
48 #[error("Invalid parameter name {} on line {}", .0, .1)]
49 InvalidParameterName(String, usize),
50 #[error("First parameter of #include should be a string on line {}", .0)]
51 FirstParamOfIncludeNotString(usize),
52 #[error("Invalid pragma '{}'", .0)]
53 InvalidPragma(String),
54 #[error("IOError while reading {}: {}", .1.display(), .0)]
55 IOError(std::io::Error, std::path::PathBuf),
56 #[cfg(feature = "vfs")]
57 #[error("VfsError: {}", .0)]
58 VfsError(#[from] vfs::VfsError),
59}
60
61type Result<T> = std::result::Result<T, Error>;
62
63pub fn parse<'a>(
73 input_file: impl AsRef<Path>,
74 base_dir: impl AsRef<Path>,
75 parameters: impl Iterator<Item = &'a str>,
76) -> Result<String> {
77 return parse_cow(input_file, base_dir, parameters);
78}
79
80#[cfg(feature = "vfs")]
95pub fn parse_vfs<'a>(
96 input_file: impl Into<VfsPath>,
97 base_dir: impl Into<VfsPath>,
98 parameters: impl Iterator<Item = &'a str>
99) -> Result<String> {
100 return parse_vfs_cow(input_file.into(), base_dir.into(), parameters);
101}
102
103pub fn parse_cow<'a, Iter, C>(
106 input_file: impl AsRef<Path>,
107 base_dir: impl AsRef<Path>,
108 parameters: Iter
109) -> Result<String>
110 where
111 Iter: Iterator<Item = C>,
112 C: Into<Cow<'a, str>>
113{
114 let content = read_to_string_std(input_file.as_ref())?;
115
116 return parse_string_cow(&content, base_dir, parameters);
117}
118
119#[cfg(feature = "vfs")]
120pub fn parse_vfs_cow<'a, Iter, C>(
121 input_file: impl Into<VfsPath>,
122 base_dir: impl Into<VfsPath>,
123 parameters: Iter
124) -> Result<String>
125 where
126 Iter: Iterator<Item = C>,
127 C: Into<Cow<'a, str>>
128{
129 let content = read_to_string(&input_file.into())?;
130
131 return parse_string_vfs_cow(&content, base_dir, parameters);
132}
133
134pub fn parse_string<'a>(
156 input: &str,
157 base_dir: impl AsRef<Path>,
158 parameters: impl Iterator<Item = &'a str>
159) -> Result<String> {
160 parse_string_cow(input, base_dir, parameters)
161}
162
163#[cfg(feature = "vfs")]
164pub fn parse_string_vfs<'a>(
165 input: &str,
166 base_dir: impl Into<VfsPath>,
167 parameters: impl Iterator<Item = &'a str>
168) -> Result<String> {
169 parse_string_vfs_cow(input, base_dir, parameters)
170}
171
172pub fn parse_string_cow<'a, Iter, C>(
175 input: &str,
176 base_dir: impl AsRef<Path>,
177 parameters: Iter
178) -> Result<String>
179 where
180 Iter: Iterator<Item = C>,
181 C: Into<Cow<'a, str>>
182{
183 #[cfg(not(feature = "vfs"))]
184 let base_dir = base_dir.as_ref();
185 #[cfg(feature = "vfs")]
186 let base_dir = VfsPath::from(vfs::PhysicalFS::new(base_dir));
187 parse_string_cow_impl(input, &base_dir, &mut parameters.map(|v| v.into()))
188}
189
190#[cfg(feature = "vfs")]
191pub fn parse_string_vfs_cow<'a, Iter, C>(
192 input: &str,
193 base_dir: impl Into<VfsPath>,
194 parameters: Iter
195) -> Result<String>
196 where
197 Iter: Iterator<Item = C>,
198 C: Into<Cow<'a, str>>
199{
200 parse_string_cow_impl(input, &base_dir.into(), &mut parameters.map(|v| v.into()))
201}
202
203fn parse_string_cow_impl<'a>(
204 input: &str,
205 base_dir: &FeatPath,
206 parameters: &mut dyn Iterator<Item = Cow<'a, str>>
207) -> Result<String> {
208 let mut replacements: Vec<(String, Cow<str>)> = vec![];
209 let mut fn_replacements: Vec<(String, Vec<String>, String)> = vec![];
210 let mut visited_sources: Vec<String> = vec![];
211
212 parse_string_cow_rec(input, None, base_dir, parameters, &mut replacements, &mut fn_replacements, &mut visited_sources)
213 .map(|str| str.unwrap())
214}
215
216fn parse_string_cow_rec<'a>(
217 input: &str,
218 path: Option<&str>,
219 base_dir: &FeatPath,
220 parameters: &mut dyn Iterator<Item = Cow<'a, str>>,
221 replacements: &mut Vec<(String, Cow<'a, str>)>,
222 fn_replacements: &mut Vec<(String, Vec<String>, String)>,
223 visited_sources: &mut Vec<String>,
224) -> Result<Option<String>> {
225 let mut out = String::new();
226
227 let mut cur_fn_replacement: Option<(String, Vec<String>, String)> = None;
228
229 let max_lines = input.chars()
230 .filter(|c| *c == '\n')
231 .count();
232
233 for (line_num, line) in input.lines().enumerate() {
234 if let Some(cur_fn_repl) = cur_fn_replacement {
235 if line.ends_with("\\") {
236 cur_fn_replacement = Some((cur_fn_repl.0, cur_fn_repl.1, cur_fn_repl.2 + &line[..line.len()-1]))
237 } else {
238 fn_replacements.push((cur_fn_repl.0, cur_fn_repl.1, cur_fn_repl.2 + line));
239 cur_fn_replacement = None;
240 }
241 continue;
242 }
243
244 let mut line_chars = line.chars().skip_while(char::is_ascii_whitespace);
245 let start_char = line_chars.by_ref().next();
246 match start_char {
247 Some('#') => {
248 let macro_name = line_chars.by_ref().take_while(|c| c.is_ascii_alphanumeric()).collect::<String>();
249 match macro_name.as_str() {
250 "define" => {
251 let mut is_last_bracket = false;
252 let name = line_chars.by_ref()
253 .skip_while(char::is_ascii_whitespace)
254 .take_while(|c| {
255 if *c == '(' {
256 is_last_bracket = true;
257 }
258 !c.is_ascii_whitespace() && *c != '('
259 })
260 .collect::<String>();
261
262 if is_last_bracket {
263 let params = line_chars.by_ref()
264 .take_while(|c| *c != ')')
265 .chunk_by(|c| *c == ',');
266 let params = params
267 .into_iter()
268 .filter(|(b, _)| !b)
269 .map(|(_, i)| i
270 .skip_while(char::is_ascii_whitespace)
271 .take_while(|c| !c.is_ascii_whitespace())
272 .collect::<String>())
273 .collect::<Vec<String>>();
274
275 let check_param_name = params.iter().find(|param| !param.chars().all(|c| c.is_alphanumeric() || c == '_'))
276 .or(params.iter().find(|param| param.len() == 0 || param.chars().next().unwrap().is_numeric()));
277 if let Some(param_name) = check_param_name {
278 return Err(Error::InvalidParameterName(param_name.clone(), line_num).into())
279 }
280
281 let replacement = line_chars.by_ref().collect::<String>();
282
283 if replacement.ends_with("\\") {
284 cur_fn_replacement = Some((name, params, replacement[..replacement.len()-1].to_string()));
285 } else {
286 fn_replacements.push((name, params, replacement));
287 }
288 } else {
289 let replacement = line_chars.collect::<Cow<str>>();
290 replacements.push((name, replacement))
291 }
292 }, "include" => {
293 let path = line_chars.by_ref()
294 .skip_while(char::is_ascii_whitespace)
295 .take_while(|c| !c.is_ascii_whitespace())
296 .collect::<String>();
297
298 if !(path.starts_with('"') && path.ends_with('"')) {
299 return Err(Error::FirstParamOfIncludeNotString(line_num).into());
300 }
301
302 let path = &path[1..path.len()-1];
303
304 let params = line_chars.by_ref()
305 .chunk_by(|c| *c == ',');
306 let mut params = params
307 .into_iter()
308 .filter(|(b, _)| !b)
309 .map(|(_, i)| Cow::Owned(i.collect::<String>()));
310
311 #[cfg(not(feature = "vfs"))]
312 let file_path = base_dir.join(path);
313
314 #[cfg(feature = "vfs")]
315 let file_path = base_dir.join(path)?;
316
317 let content = read_to_string(&file_path)?;
318
319 match parse_string_cow_rec(&content, Some(path), &base_dir, &mut params, replacements, fn_replacements, visited_sources) {
320 Ok(Some(res)) => {
321 out += res.as_str();
322 visited_sources.push(path.to_string());
323 },
324 Ok(None) => {},
325 Err(err) => return Err(err),
326 };
327 }, "param" => {
328 let param_name = line_chars.by_ref()
329 .skip_while(|c| c.is_ascii_whitespace())
330 .take_while(|c| !c.is_ascii_whitespace())
331 .collect::<String>();
332
333 if !line_chars.by_ref().all(|c| c.is_ascii_whitespace()) {
334 return Err(Error::ExtraParamsInMacro(line_num, "param"));
335 }
336
337 let Some(param_value) = parameters.next() else {
338 return Err(Error::NotEnoughParameters);
339 };
340
341 replacements.push((param_name, param_value));
342 }, "pragma" => {
343 let param_name = line_chars.by_ref()
344 .skip_while(|c| c.is_ascii_whitespace())
345 .take_while(|c| !c.is_ascii_whitespace())
346 .collect::<String>();
347
348 if !line_chars.by_ref().all(|c| c.is_ascii_whitespace()) {
349 return Err(Error::ExtraParamsInMacro(line_num, "pragma"));
350 }
351
352 if param_name != "once" {
353 return Err(Error::InvalidPragma(param_name));
354 }
355
356 if let Some(path) = path {
357 if visited_sources.iter().find(|p| p.as_str() == path).is_some() {
358 return Ok(None);
359 }
360 }
361 },
362 _ => return Err(Error::InvalidMacro(macro_name, line_num)),
363 }
364 },
365 Some('\\') if (line_chars.next() == Some('#')) => {
366 out += fn_replace(replace(&line.replacen("\\#", "#", 1), &replacements), &fn_replacements)?.as_ref();
367 if line_num != max_lines {
368 out += "\n";
369 }
370 },
371 _ => {
372 out += fn_replace(replace(line, &replacements), &fn_replacements)?.as_ref();
373 if line_num != max_lines {
374 out += "\n";
375 }
376 },
377 }
378 }
379
380 if parameters.count() != 0 {
381 return Err(Error::UnusedParameters);
382 }
383
384 return Ok(Some(out));
385}
386
387fn ident_range(str: &str, start: usize, end: usize) -> Option<(usize, usize)> {
389 if (start == 0 || str.chars().nth(start - 1).map(|c| !(c.is_alphanumeric() || c == '_')).unwrap_or(true))
390 && str.chars().nth(end).map(|c| !(c.is_alphanumeric() || c == '_')).unwrap_or(true)
391 {
392 return Some((start, end));
393 } else {
394 return None;
395 }
396}
397
398fn ident_or_paste_range(str: &str, start: usize, end: usize) -> Option<(usize, usize)> {
399 if start >= 2 && str.chars().skip(start - 2).take(2).all(|c| c == '#') {
400 let mut iter = str.chars().skip(end).take(2).tee();
401 if iter.0.count() == 2 && iter.1.all(|c| c == '#') {
403 Some((start - 2, end + 2))
404 } else if str.chars().nth(end).map(|c| !(c.is_alphanumeric() || c == '_')).unwrap_or(true) {
406 Some((start - 2, end))
407 } else {
408 None
409 }
410 } else if start == 0 || str.chars().nth(start - 1).map(|c| !(c.is_alphanumeric() || c == '_')).unwrap_or(true) {
411 let mut iter = str.chars().skip(end).take(2).tee();
412 if iter.0.count() == 2 && iter.1.all(|c| c == '#') {
414 Some((start, end + 2))
415 } else if str.chars().nth(end).map(|c| !(c.is_alphanumeric() || c == '_')).unwrap_or(true) {
417 Some((start, end))
418 } else {
419 None
420 }
421 } else {
422 None
423 }
424}
425
426fn replace<'a>(line: &'a str, replacements: &Vec<(String, Cow<str>)>) -> Cow<'a, str> {
427 let mut out: Cow<str> = line.into();
428
429 for replacement in replacements {
430 out = replace_all(out, &replacement.0, replacement.1.as_ref(), ident_range);
431 }
432
433 return out;
434}
435
436fn fn_replace<'a>(line: Cow<'a, str>, replacements: &Vec<(String, Vec<String>, String)>) -> Result<Cow<'a, str>> {
437 let mut out: Cow<str> = line;
438
439 for replacement in replacements {
440 out = replace_all_fn(out, replacement.0.as_str(), replacement.2.as_str(), &replacement.1, ident_or_paste_range)?;
441 }
442
443 return Ok(out);
444}
445
446fn replace_all<'a>(str: Cow<'a, str>, to_match: &str, replacement: &str, predicate: impl Fn(&str, usize, usize) -> Option<(usize, usize)>) -> Cow<'a, str> {
447 let matches = str.match_indices(to_match).collect::<Vec<_>>();
448
449 let mut out: Option<Cow<str>> = None;
450 let mut end_idx = str.len();
451
452 for (idx, _) in matches.into_iter().rev() {
453 if let Some((start, end)) = predicate(str.as_ref(), idx, idx + to_match.len()) {
454 let following_str = &str[end..end_idx];
455 end_idx = start;
456 out = out.map(|m| concat_string!(replacement, following_str, m.as_ref()).into())
457 .or(Some(concat_string!(replacement, following_str).into()));
458 }
459 }
460
461 if end_idx != 0 {
462 out = out.map(|m| concat_string!(&str[0..end_idx], m.as_ref()).into())
463 }
464
465 return out.unwrap_or(str);
466}
467
468fn replace_all_fn<'a>(
469 str: Cow<'a, str>,
470 name: &str,
471 replacement: &str,
472 param_names: &Vec<String>,
473 predicate: impl Fn(&str, usize, usize) -> Option<(usize, usize)>,
474) -> Result<Cow<'a, str>> {
475 let matches = str.match_indices(name).collect::<Vec<_>>();
476
477 let mut out: Option<Cow<str>> = None;
478 let mut end_idx = str.len();
479
480 for (idx, _) in matches.into_iter().rev() {
481 let mut iter = str.chars();
482 if iter.by_ref().nth(idx + name.len()) != Some('(') {
483 continue;
484 }
485
486 let mut parens = 0;
487 let mut params = Vec::new();
488 let mut cur = String::new();
489 let mut param_len = 0;
490 for (i, c) in iter.enumerate() {
491 if c == ')' {
492 parens -= 1;
493 if parens == -1 {
494 params.push(cur);
495 cur = String::new();
496 param_len = i;
497 break;
498 }
499 }
500
501 if c == ',' && parens == 0 {
503 params.push(cur);
504 cur = String::new();
505 continue;
506 }
507
508 if c == '(' {
509 parens += 1;
510 }
511
512 cur.push(c);
513 }
514
515 let to_replace_len = name.len() + 2 + param_len;
516
517 let Some((start, end)) = predicate(str.as_ref(), idx, idx + to_replace_len) else {
518 continue;
519 };
520
521 if params.len() < param_names.len() {
522 return Err(Error::NotEnoughParametersMacro(name.to_string()));
523 } else if params.len() > param_names.len() {
524 return Err(Error::UnusedParametersMacro(name.to_string()))
525 }
526
527 let params = param_names.iter()
528 .zip(params.iter());
529
530 let mut replacement = Cow::Borrowed(replacement);
531 for param in params {
532 replacement = replace_all(replacement, param.0, param.1, ident_or_paste_range);
533 }
534
535 let following_str = &str[end..end_idx];
536 end_idx = start;
537 out = out.map(|m| concat_string!(replacement, following_str, m.as_ref()).into())
538 .or(Some(concat_string!(replacement, following_str).into()));
539 }
540
541 if end_idx != 0 {
542 out = out.map(|m| concat_string!(&str[0..end_idx], m.as_ref()).into());
543 }
544
545 return Ok(out.unwrap_or(str));
546}