Skip to main content

ppx_impl/
lib.rs

1use std::borrow::Cow;
2use std::path::Path;
3// use std::path::{Path, PathBuf};
4
5use 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
63/// Parses a file using the templating engine.
64///
65/// For an example, see [parse_string].
66///
67/// # Parameters
68/// - `input_file`: the file that is read
69/// - `base_dir`: all includes are resolved relative to this directory
70/// - `parameters`: if `input_file` contains any parameter macros, pass an iterator
71///                 to them here. Otherwise pass `std::iter::empty()`.
72pub 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/// Parse a file in a virtual filesystem using the templating engine.
81///
82/// # Examples
83///
84/// ```rust
85/// # use ppx_impl::parse_vfs;
86/// # use vfs::VfsPath;
87/// let root: VfsPath = vfs::MemoryFS::new().into();
88/// let path = root.join("test.txt").unwrap();
89///
90/// path.create_file().unwrap().write_all(b"#param A\nHello A!");
91/// let result = parse_vfs(path, root, ["world"].into_iter()).unwrap();
92/// assert_eq!(result, "Hello world!");
93/// ```
94#[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
103/// Same as [parse], but the parameters iterator has an item of `impl Into<Cow<str>>`.
104/// This is so that an empty iterator can be passed to `parse`.
105pub 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
134/// Parses a file using the templating engine.
135///
136/// # Parameters
137/// - `input`: the contents to process
138/// - `base_dir`: all includes are resolved relative to this directory
139/// - `parameters`: if `input` contains any parameter macros, pass an iterator
140///                 to them here. Otherwise pass `std::iter::empty()`.
141///
142/// # Example
143///
144/// ```rust
145/// # use ppx_impl::*;
146/// # #[cfg(not(feature = "vfs"))]
147/// let res = parse_string(
148///     "#define A 4\nThe answer is A",
149///     std::env::current_dir().unwrap(),
150///     std::iter::empty()
151/// ).unwrap();
152/// # #[cfg(not(feature = "vfs"))]
153/// assert_eq!(res, "The answer is 4");
154/// ```
155pub 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
172/// Same as [parse_string], but the parameters iterator has an item of `impl Into<Cow<str>>`.
173/// This is so that an empty iterator can be passed to `parse_string`.
174pub 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 is_ident(str: &str, start: usize, end: usize) -> bool {
388    (start == 0 || str.chars().nth(start - 1).map(|c| !(c.is_alphanumeric() || c == '_')).unwrap_or(true))
389        && str.chars().nth(end).map(|c| !(c.is_alphanumeric() || c == '_')).unwrap_or(true)
390}
391
392fn replace<'a>(line: &'a str, replacements: &Vec<(String, Cow<str>)>) -> Cow<'a, str> {
393    let mut out: Cow<str> = line.into();
394
395    for replacement in replacements {
396        out = replace_all(out, &replacement.0, replacement.1.as_ref(), is_ident);
397    }
398
399    return out;
400}
401
402fn fn_replace<'a>(line: Cow<'a, str>, replacements: &Vec<(String, Vec<String>, String)>) -> Result<Cow<'a, str>> {
403    let mut out: Cow<str> = line;
404
405    for replacement in replacements {
406        out = replace_all_fn(out, replacement.0.as_str(), replacement.2.as_str(), &replacement.1, is_ident)?;
407    }
408
409    return Ok(out);
410}
411
412fn replace_all<'a>(str: Cow<'a, str>, to_match: &str, replacement: &str, predicate: impl Fn(&str, usize, usize) -> bool) -> Cow<'a, str> {
413    let matches = str.match_indices(to_match).collect::<Vec<_>>();
414
415    let mut out: Option<Cow<str>> = None;
416    let mut end_idx = str.len();
417
418    for (idx, _) in matches.into_iter().rev() {
419        if predicate(str.as_ref(), idx, idx + to_match.len()) {
420            let following_str = &str[idx + to_match.len()..end_idx];
421            end_idx = idx;
422            out = out.map(|m| concat_string!(replacement, following_str, m.as_ref()).into())
423                .or(Some(concat_string!(replacement, following_str).into()));
424        }
425    }
426
427    if end_idx != 0 {
428        out = out.map(|m| concat_string!(&str[0..end_idx], m.as_ref()).into())
429    }
430
431    return out.unwrap_or(str);
432}
433
434fn replace_all_fn<'a>(
435    str: Cow<'a, str>,
436    name: &str,
437    replacement: &str,
438    param_names: &Vec<String>,
439    predicate: impl Fn(&str, usize, usize) -> bool,
440) -> Result<Cow<'a, str>> {
441    let matches = str.match_indices(name).collect::<Vec<_>>();
442
443    let mut out: Option<Cow<str>> = None;
444    let mut end_idx = str.len();
445
446    for (idx, _) in matches.into_iter().rev() {
447        let mut iter = str.chars();
448        if iter.by_ref().nth(idx + name.len()) != Some('(') {
449            continue;
450        }
451
452        let mut parens = 0;
453        let mut params = Vec::new();
454        let mut cur = String::new();
455        let mut param_len = 0;
456        for (i, c) in iter.enumerate() {
457            if c == ')' {
458                parens -= 1;
459                if parens == -1 {
460                    params.push(cur);
461                    cur = String::new();
462                    param_len = i;
463                    break;
464                }
465            }
466
467            // next param
468            if c == ',' && parens == 0 {
469                params.push(cur);
470                cur = String::new();
471                continue;
472            }
473
474            if c == '(' {
475                parens += 1;
476            }
477
478            cur.push(c);
479        }
480
481        let to_replace_len = name.len() + 2 + param_len;
482
483        if !predicate(str.as_ref(), idx, idx + to_replace_len) {
484            continue;
485        }
486
487        if params.len() < param_names.len() {
488            return Err(Error::NotEnoughParametersMacro(name.to_string()));
489        } else if params.len() > param_names.len() {
490            return Err(Error::UnusedParametersMacro(name.to_string()))
491        }
492
493        let params = param_names.iter()
494            .zip(params.iter());
495
496        let mut replacement = Cow::Borrowed(replacement);
497        for param in params {
498            replacement = replace_all(replacement, param.0, param.1, is_ident);
499        }
500
501        let following_str = &str[idx + to_replace_len..end_idx];
502        end_idx = idx;
503        out = out.map(|m| concat_string!(replacement, following_str, m.as_ref()).into())
504            .or(Some(concat_string!(replacement, following_str).into()));
505    }
506
507    if end_idx != 0 {
508        out = out.map(|m| concat_string!(&str[0..end_idx], m.as_ref()).into());
509    }
510
511    return Ok(out.unwrap_or(str));
512}