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    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
61/// Parses a file using the templating engine.
62///
63/// For an example, see [parse_string].
64///
65/// # Parameters
66/// - `input_file`: the file that is read
67/// - `base_dir`: all includes are resolved relative to this directory
68/// - `parameters`: if `input_file` contains any parameter macros, pass an iterator
69///                 to them here. Otherwise pass `std::iter::empty()`.
70pub 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/// Parse a file in a virtual filesystem using the templating engine.
79///
80/// # Examples
81///
82/// ```rust
83/// # use ppx_impl::parse_vfs;
84/// # use vfs::VfsPath;
85/// let root: VfsPath = vfs::MemoryFS::new().into();
86/// let path = root.join("test.txt").unwrap();
87///
88/// path.create_file().unwrap().write_all(b"#param A\nHello A!");
89/// let result = parse_vfs(path, root, ["world"].into_iter()).unwrap();
90/// assert_eq!(result, "Hello world!");
91/// ```
92#[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
101/// Same as [parse], but the parameters iterator has an item of `impl Into<Cow<str>>`.
102/// This is so that an empty iterator can be passed to `parse`.
103pub 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
132/// Parses a file using the templating engine.
133///
134/// # Parameters
135/// - `input`: the contents to process
136/// - `base_dir`: all includes are resolved relative to this directory
137/// - `parameters`: if `input` contains any parameter macros, pass an iterator
138///                 to them here. Otherwise pass `std::iter::empty()`.
139///
140/// # Example
141///
142/// ```rust
143/// # use ppx_impl::*;
144/// # #[cfg(not(feature = "vfs"))]
145/// let res = parse_string(
146///     "#define A 4\nThe answer is A",
147///     std::env::current_dir().unwrap(),
148///     std::iter::empty()
149/// ).unwrap();
150/// # #[cfg(not(feature = "vfs"))]
151/// assert_eq!(res, "The answer is 4");
152/// ```
153pub 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
170/// Same as [parse_string], but the parameters iterator has an item of `impl Into<Cow<str>>`.
171/// This is so that an empty iterator can be passed to `parse_string`.
172pub 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            // next param
425            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}