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")]
91pub fn parse_vfs<'a>(
92 input_file: impl Into<VfsPath>,
93 base_dir: impl Into<VfsPath>,
94 parameters: impl Iterator<Item = &'a str>
95) -> Result<String> {
96 return parse_vfs_cow(input_file.into(), base_dir.into(), parameters);
97}
98
99pub fn parse_cow<'a, Iter, C>(
102 input_file: impl AsRef<Path>,
103 base_dir: impl AsRef<Path>,
104 parameters: Iter
105) -> Result<String>
106 where
107 Iter: Iterator<Item = C>,
108 C: Into<Cow<'a, str>>
109{
110 let content = read_to_string_std(input_file.as_ref())?;
111
112 return parse_string_cow(&content, base_dir, parameters);
113}
114
115#[cfg(feature = "vfs")]
116pub fn parse_vfs_cow<'a, Iter, C>(
117 input_file: impl Into<VfsPath>,
118 base_dir: impl Into<VfsPath>,
119 parameters: Iter
120) -> Result<String>
121 where
122 Iter: Iterator<Item = C>,
123 C: Into<Cow<'a, str>>
124{
125 let content = read_to_string(&input_file.into())?;
126
127 return parse_string_vfs_cow(&content, base_dir, parameters);
128}
129
130pub fn parse_string<'a>(
152 input: &str,
153 base_dir: impl AsRef<Path>,
154 parameters: impl Iterator<Item = &'a str>
155) -> Result<String> {
156 parse_string_cow(input, base_dir, parameters)
157}
158
159#[cfg(feature = "vfs")]
160pub fn parse_string_vfs<'a>(
161 input: &str,
162 base_dir: impl Into<VfsPath>,
163 parameters: impl Iterator<Item = &'a str>
164) -> Result<String> {
165 parse_string_vfs_cow(input, base_dir, parameters)
166}
167
168pub fn parse_string_cow<'a, Iter, C>(
171 input: &str,
172 base_dir: impl AsRef<Path>,
173 parameters: Iter
174) -> Result<String>
175 where
176 Iter: Iterator<Item = C>,
177 C: Into<Cow<'a, str>>
178{
179 #[cfg(not(feature = "vfs"))]
180 let base_dir = base_dir.as_ref();
181 #[cfg(feature = "vfs")]
182 let base_dir = VfsPath::from(vfs::PhysicalFS::new(base_dir));
183 parse_string_cow_impl(input, &base_dir, &mut parameters.map(|v| v.into()))
184}
185
186#[cfg(feature = "vfs")]
187pub fn parse_string_vfs_cow<'a, Iter, C>(
188 input: &str,
189 base_dir: impl Into<VfsPath>,
190 parameters: Iter
191) -> Result<String>
192 where
193 Iter: Iterator<Item = C>,
194 C: Into<Cow<'a, str>>
195{
196 parse_string_cow_impl(input, &base_dir.into(), &mut parameters.map(|v| v.into()))
197}
198
199fn parse_string_cow_impl<'a>(
200 input: &str,
201 base_dir: &FeatPath,
202 parameters: &mut dyn Iterator<Item = Cow<'a, str>>
203) -> Result<String> {
204 let mut out = String::new();
205
206 let mut replacements: Vec<(String, Cow<str>)> = vec![];
207 let mut fn_replacements: Vec<(String, Vec<String>, String)> = vec![];
208 let mut cur_fn_replacement: Option<(String, Vec<String>, String)> = None;
209
210 let max_lines = input.chars()
211 .filter(|c| *c == '\n')
212 .count();
213
214 for (line_num, line) in input.lines().enumerate() {
215 if let Some(cur_fn_repl) = cur_fn_replacement {
216 if line.ends_with("\\") {
217 cur_fn_replacement = Some((cur_fn_repl.0, cur_fn_repl.1, cur_fn_repl.2 + &line[..line.len()-1]))
218 } else {
219 fn_replacements.push((cur_fn_repl.0, cur_fn_repl.1, cur_fn_repl.2 + line));
220 cur_fn_replacement = None;
221 }
222 continue;
223 }
224
225 let mut line_chars = line.chars().skip_while(char::is_ascii_whitespace);
226 let start_char = line_chars.by_ref().next();
227 match start_char {
228 Some('#') => {
229 let macro_name = line_chars.by_ref().take_while(|c| c.is_ascii_alphanumeric()).collect::<String>();
230 match macro_name.as_str() {
231 "define" => {
232 let mut is_last_bracket = false;
233 let name = line_chars.by_ref()
234 .skip_while(char::is_ascii_whitespace)
235 .take_while(|c| {
236 if *c == '(' {
237 is_last_bracket = true;
238 }
239 !c.is_ascii_whitespace() && *c != '('
240 })
241 .collect::<String>();
242
243 if is_last_bracket {
244 let params = line_chars.by_ref()
245 .take_while(|c| *c != ')')
246 .chunk_by(|c| *c == ',');
247 let params = params
248 .into_iter()
249 .filter(|(b, _)| !b)
250 .map(|(_, i)| i
251 .skip_while(char::is_ascii_whitespace)
252 .take_while(|c| !c.is_ascii_whitespace())
253 .collect::<String>())
254 .collect::<Vec<String>>();
255
256 let check_param_name = params.iter().find(|param| !param.chars().all(|c| c.is_alphanumeric() || c == '_'))
257 .or(params.iter().find(|param| param.len() == 0 || param.chars().next().unwrap().is_numeric()));
258 if let Some(param_name) = check_param_name {
259 return Err(Error::InvalidParameterName(param_name.clone(), line_num))
260 }
261
262 let replacement = line_chars.by_ref().collect::<String>();
263
264 if replacement.ends_with("\\") {
265 cur_fn_replacement = Some((name, params, replacement[..replacement.len()-1].to_string()));
266 } else {
267 fn_replacements.push((name, params, replacement));
268 }
269 } else {
270 let replacement = line_chars.collect::<Cow<str>>();
271 replacements.push((name, replacement))
272 }
273 }, "include" => {
274 let path = line_chars.by_ref()
275 .skip_while(char::is_ascii_whitespace)
276 .take_while(|c| !c.is_ascii_whitespace())
277 .collect::<String>();
278
279 if !(path.starts_with('"') && path.ends_with('"')) {
280 return Err(Error::FirstParamOfIncludeNotString(line_num));
281 }
282
283 let path = &path[1..path.len()-1];
284
285 let params = line_chars.by_ref()
286 .chunk_by(|c| *c == ',');
287 let mut params = params
288 .into_iter()
289 .filter(|(b, _)| !b)
290 .map(|(_, i)| Cow::Owned(i.collect::<String>()));
291
292 #[cfg(not(feature = "vfs"))]
293 let file_path = base_dir.join(path);
294
295 #[cfg(feature = "vfs")]
296 let file_path = base_dir.join(path)?;
297
298 let content = read_to_string(&file_path)?;
299
300 out += parse_string_cow_impl(&content, &base_dir, &mut params)?.as_str();
301 }, "param" => {
302 let param_name = line_chars.by_ref()
303 .skip_while(|c| c.is_ascii_whitespace())
304 .take_while(|c| !c.is_ascii_whitespace())
305 .collect::<String>();
306
307 if !line_chars.by_ref().all(|c| c.is_ascii_whitespace()) {
308 return Err(Error::ExtraParamsInParamMacro(line_num));
309 }
310
311 let Some(param_value) = parameters.next() else {
312 return Err(Error::NotEnoughParameters);
313 };
314
315 replacements.push((param_name, param_value.into()));
316 },
317 _ => return Err(Error::InvalidMacro(macro_name, line_num)),
318 }
319 },
320 Some('\\') if (line_chars.next() == Some('#')) => {
321 out += fn_replace(replace(&line.replacen("\\#", "#", 1), &replacements), &fn_replacements)?.as_ref();
322 if line_num != max_lines {
323 out += "\n";
324 }
325 },
326 _ => {
327 out += fn_replace(replace(line, &replacements), &fn_replacements)?.as_ref();
328 if line_num != max_lines {
329 out += "\n";
330 }
331 },
332 }
333 }
334
335 if parameters.count() != 0 {
336 return Err(Error::UnusedParameters);
337 }
338
339 return Ok(out);
340}
341
342fn is_ident(str: &str, start: usize, end: usize) -> bool {
343 (start == 0 || str.chars().nth(start - 1).map(|c| !(c.is_alphanumeric() || c == '_')).unwrap_or(true))
344 && str.chars().nth(end).map(|c| !(c.is_alphanumeric() || c == '_')).unwrap_or(true)
345}
346
347fn replace<'a>(line: &'a str, replacements: &Vec<(String, Cow<str>)>) -> Cow<'a, str> {
348 let mut out: Cow<str> = line.into();
349
350 for replacement in replacements {
351 out = replace_all(out, &replacement.0, replacement.1.as_ref(), is_ident);
352 }
353
354 return out;
355}
356
357fn fn_replace<'a>(line: Cow<'a, str>, replacements: &Vec<(String, Vec<String>, String)>) -> Result<Cow<'a, str>> {
358 let mut out: Cow<str> = line;
359
360 for replacement in replacements {
361 out = replace_all_fn(out, replacement.0.as_str(), replacement.2.as_str(), &replacement.1, is_ident)?;
362 }
363
364 return Ok(out);
365}
366
367fn replace_all<'a>(str: Cow<'a, str>, to_match: &str, replacement: &str, predicate: impl Fn(&str, usize, usize) -> bool) -> Cow<'a, str> {
368 let matches = str.match_indices(to_match).collect::<Vec<_>>();
369
370 let mut out: Option<Cow<str>> = None;
371 let mut end_idx = str.len();
372
373 for (idx, _) in matches.into_iter().rev() {
374 if predicate(str.as_ref(), idx, idx + to_match.len()) {
375 let following_str = &str[idx + to_match.len()..end_idx];
376 end_idx = idx;
377 out = out.map(|m| concat_string!(replacement, following_str, m.as_ref()).into())
378 .or(Some(concat_string!(replacement, following_str).into()));
379 }
380 }
381
382 if end_idx != 0 {
383 out = out.map(|m| concat_string!(&str[0..end_idx], m.as_ref()).into())
384 }
385
386 return out.unwrap_or(str);
387}
388
389fn replace_all_fn<'a>(
390 str: Cow<'a, str>,
391 name: &str,
392 replacement: &str,
393 param_names: &Vec<String>,
394 predicate: impl Fn(&str, usize, usize) -> bool
395) -> Result<Cow<'a, str>> {
396 let matches = str.match_indices(name).collect::<Vec<_>>();
397
398 let mut out: Option<Cow<str>> = None;
399 let mut end_idx = str.len();
400
401 for (idx, _) in matches.into_iter().rev() {
402 let mut iter = str.chars();
403 if iter.by_ref().nth(idx + name.len()) != Some('(') {
404 continue;
405 }
406
407 let iter = iter.tee();
408 let iter2 = iter.1.tee();
409 let iters = (iter.0, iter2.0, iter2.1);
410
411 let mut params = Vec::new();
412 let mut cur = String::new();
413 let mut param_len = 0;
414 for (i, ((prev, c), next)) in std::iter::zip(
415 std::iter::zip([' '].into_iter().chain(iters.0),
416 iters.1
417 ), iters.2.skip(1).chain([' '].into_iter())
418 ).enumerate() {
419 if c == ')' && prev != '\\' {
420 params.push(cur);
421 cur = String::new();
422 param_len = i;
423 break;
424 }
425
426 if c == '\\' && ['(', ')', ','].contains(&next) {
427 continue;
428 }
429
430 if c == ',' && prev != '\\' {
431 params.push(cur);
432 cur = String::new();
433 continue;
434 }
435
436 cur.push(c);
437 }
438
439 let to_replace_len = name.len() + 2 + param_len;
440
441 if !predicate(str.as_ref(), idx, idx + to_replace_len) {
442 continue;
443 }
444
445 if params.len() != param_names.len() {
446 return Err(Error::NotEnoughParametersMacro(name.to_string()));
447 }
448
449 let params = param_names.iter()
450 .zip(params.iter());
451
452 let mut replacement = Cow::Borrowed(replacement);
453 for param in params {
454 replacement = replace_all(replacement, param.0, param.1, is_ident);
455 }
456
457 let following_str = &str[idx + to_replace_len..end_idx];
458 end_idx = idx;
459 out = out.map(|m| concat_string!(replacement, following_str, m.as_ref()).into())
460 .or(Some(concat_string!(replacement, following_str).into()));
461 }
462
463 if end_idx != 0 {
464 out = out.map(|m| concat_string!(&str[0..end_idx], m.as_ref()).into());
465 }
466
467 return Ok(out.unwrap_or(str));
468}