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