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("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
59/// Parses a file using the templating engine.
60///
61/// For an example, see [parse_string].
62///
63/// # Parameters
64/// - `input_file`: the file that is read
65/// - `base_dir`: all includes are resolved relative to this directory
66/// - `parameters`: if `input_file` contains any parameter macros, pass an iterator
67///                 to them here. Otherwise pass `std::iter::empty()`.
68pub 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")]
77pub fn parse_vfs<'a>(
78    input_file: impl Into<VfsPath>,
79    base_dir: impl Into<VfsPath>,
80    parameters: impl Iterator<Item = &'a str>
81) -> Result<String> {
82    return parse_vfs_cow(input_file.into(), base_dir.into(), parameters);
83}
84
85/// Same as [parse], but the parameters iterator has an item of `impl Into<Cow<str>>`.
86/// This is so that an empty iterator can be passed to `parse`.
87pub fn parse_cow<'a, Iter, C>(
88    input_file: impl AsRef<Path>,
89    base_dir: impl AsRef<Path>,
90    parameters: Iter
91) -> Result<String>
92    where
93        Iter: Iterator<Item = C>,
94        C: Into<Cow<'a, str>>
95{
96    let content = read_to_string_std(input_file.as_ref())?;
97
98    return parse_string_cow(&content, base_dir, parameters);
99}
100
101#[cfg(feature = "vfs")]
102pub fn parse_vfs_cow<'a, Iter, C>(
103    input_file: impl Into<VfsPath>,
104    base_dir: impl Into<VfsPath>,
105    parameters: Iter
106) -> Result<String>
107    where
108        Iter: Iterator<Item = C>,
109        C: Into<Cow<'a, str>>
110{
111    let content = read_to_string(&input_file.into())?;
112
113    return parse_string_vfs_cow(&content, base_dir, parameters);
114}
115
116/// Parses a file using the templating engine.
117///
118/// # Parameters
119/// - `input`: the contents to process
120/// - `base_dir`: all includes are resolved relative to this directory
121/// - `parameters`: if `input` contains any parameter macros, pass an iterator
122///                 to them here. Otherwise pass `std::iter::empty()`.
123///
124/// # Example
125///
126/// ```rust
127/// # use ppx_impl::*;
128/// # #[cfg(not(feature = "vfs"))]
129/// let res = parse_string(
130///     "#define A 4\nThe answer is A",
131///     std::env::current_dir().unwrap(),
132///     std::iter::empty()
133/// ).unwrap();
134/// # #[cfg(not(feature = "vfs"))]
135/// assert_eq!(res, "The answer is 4");
136/// ```
137pub fn parse_string<'a>(
138    input: &str,
139    base_dir: impl AsRef<Path>,
140    parameters: impl Iterator<Item = &'a str>
141) -> Result<String> {
142    parse_string_cow(input, base_dir, parameters)
143}
144
145#[cfg(feature = "vfs")]
146pub fn parse_string_vfs<'a>(
147    input: &str,
148    base_dir: impl Into<VfsPath>,
149    parameters: impl Iterator<Item = &'a str>
150) -> Result<String> {
151    parse_string_vfs_cow(input, base_dir, parameters)
152}
153
154/// Same as [parse_string], but the parameters iterator has an item of `impl Into<Cow<str>>`.
155/// This is so that an empty iterator can be passed to `parse_string`.
156pub fn parse_string_cow<'a, Iter, C>(
157    input: &str,
158    base_dir: impl AsRef<Path>,
159    parameters: Iter
160) -> Result<String>
161    where
162        Iter: Iterator<Item = C>,
163        C: Into<Cow<'a, str>>
164{
165    #[cfg(not(feature = "vfs"))]
166    let base_dir = base_dir.as_ref();
167    #[cfg(feature = "vfs")]
168    let base_dir = VfsPath::from(vfs::PhysicalFS::new(base_dir));
169    parse_string_cow_impl(input, &base_dir, &mut parameters.map(|v| v.into()))
170}
171
172#[cfg(feature = "vfs")]
173pub fn parse_string_vfs_cow<'a, Iter, C>(
174    input: &str,
175    base_dir: impl Into<VfsPath>,
176    parameters: Iter
177) -> Result<String>
178    where
179        Iter: Iterator<Item = C>,
180        C: Into<Cow<'a, str>>
181{
182    parse_string_cow_impl(input, &base_dir.into(), &mut parameters.map(|v| v.into()))
183}
184
185fn parse_string_cow_impl<'a>(
186    input: &str,
187    base_dir: &FeatPath,
188    parameters: &mut dyn Iterator<Item = Cow<'a, str>>
189) -> Result<String> {
190    let mut out = String::new();
191
192    let mut replacements: Vec<(String, Cow<str>)> = vec![];
193    let mut fn_replacements: Vec<(String, Vec<String>, String)> = vec![];
194    let mut cur_fn_replacement: Option<(String, Vec<String>, String)> = None;
195
196    let max_lines = input.chars()
197        .filter(|c| *c == '\n')
198        .count();
199
200    for (line_num, line) in input.lines().enumerate() {
201        if let Some(cur_fn_repl) = cur_fn_replacement {
202            if line.ends_with("\\") {
203                cur_fn_replacement = Some((cur_fn_repl.0, cur_fn_repl.1, cur_fn_repl.2 + &line[..line.len()-1]))
204            } else {
205                fn_replacements.push((cur_fn_repl.0, cur_fn_repl.1, cur_fn_repl.2 + line));
206                cur_fn_replacement = None;
207            }
208            continue;
209        }
210
211        let mut line_chars = line.chars().skip_while(char::is_ascii_whitespace);
212        let start_char = line_chars.by_ref().next();
213        match start_char {
214            Some('#') => {
215                let macro_name = line_chars.by_ref().take_while(|c| c.is_ascii_alphanumeric()).collect::<String>();
216                match macro_name.as_str() {
217                    "define" => {
218                        let mut is_last_bracket = false;
219                        let name = line_chars.by_ref()
220                            .skip_while(char::is_ascii_whitespace)
221                            .take_while(|c| {
222                                if *c == '(' {
223                                    is_last_bracket = true;
224                                }
225                                !c.is_ascii_whitespace() && *c != '('
226                            })
227                            .collect::<String>();
228
229                        if is_last_bracket {
230                            let params = line_chars.by_ref()
231                                .take_while(|c| *c != ')')
232                                .chunk_by(|c| *c == ',');
233                            let params = params
234                                .into_iter()
235                                .filter(|(b, _)| !b)
236                                .map(|(_, i)| i
237                                    .skip_while(char::is_ascii_whitespace)
238                                    .take_while(|c| !c.is_ascii_whitespace())
239                                    .collect::<String>())
240                                .collect::<Vec<String>>();
241
242                            let check_param_name = params.iter().find(|param| !param.chars().all(|c| c.is_alphanumeric() || c == '_'))
243                                .or(params.iter().find(|param| param.len() == 0 || param.chars().next().unwrap().is_numeric()));
244                            if let Some(param_name) = check_param_name {
245                                return Err(Error::InvalidParameterName(param_name.clone(), line_num))
246                            }
247
248                            let replacement = line_chars.by_ref().collect::<String>();
249
250                            if replacement.ends_with("\\") {
251                                cur_fn_replacement = Some((name, params, replacement[..replacement.len()-1].to_string()));
252                            } else {
253                                fn_replacements.push((name, params, replacement));
254                            }
255                        } else {
256                            let replacement = line_chars.collect::<Cow<str>>();
257                            replacements.push((name, replacement))
258                        }
259                    }, "include" => {
260                        let path = line_chars.by_ref()
261                            .skip_while(char::is_ascii_whitespace)
262                            .take_while(|c| !c.is_ascii_whitespace())
263                            .collect::<String>();
264
265                        if !(path.starts_with('"') && path.ends_with('"')) {
266                            return Err(Error::FirstParamOfIncludeNotString(line_num));
267                        }
268
269                        let path = &path[1..path.len()-1];
270
271                        let params = line_chars.by_ref()
272                            .chunk_by(|c| *c == ',');
273                        let mut params = params
274                            .into_iter()
275                            .filter(|(b, _)| !b)
276                            .map(|(_, i)| Cow::Owned(i.collect::<String>()));
277
278                        #[cfg(not(feature = "vfs"))]
279                        let file_path = base_dir.join(path);
280
281                        #[cfg(feature = "vfs")]
282                        let file_path = base_dir.join(path)?;
283
284                        let content = read_to_string(&file_path)?;
285
286                        out += parse_string_cow_impl(&content, &base_dir, &mut params)?.as_str();
287                    }, "param" => {
288                        let param_name = line_chars.by_ref()
289                            .skip_while(|c| c.is_ascii_whitespace())
290                            .take_while(|c| !c.is_ascii_whitespace())
291                            .collect::<String>();
292
293                        if !line_chars.by_ref().all(|c| c.is_ascii_whitespace()) {
294                            return Err(Error::ExtraParamsInParamMacro(line_num));
295                        }
296
297                        let Some(param_value) = parameters.next() else {
298                            return Err(Error::NotEnoughParameters);
299                        };
300
301                        replacements.push((param_name, param_value.into()));
302                    },
303                    _ => return Err(Error::InvalidMacro(macro_name, line_num)),
304                }
305            },
306            Some('\\') if (line_chars.next() == Some('#')) => {
307                out += fn_replace(replace(&line.replacen("\\#", "#", 1), &replacements), &fn_replacements)?.as_ref();
308                if line_num != max_lines {
309                    out += "\n";
310                }
311            },
312            _ => {
313                out += fn_replace(replace(line, &replacements), &fn_replacements)?.as_ref();
314                if line_num != max_lines {
315                    out += "\n";
316                }
317            },
318        }
319    }
320
321    if parameters.count() != 0 {
322        return Err(Error::UnusedParameters);
323    }
324
325    return Ok(out);
326}
327
328fn is_ident(str: &str, start: usize, end: usize) -> bool {
329    (start == 0 || str.chars().nth(start - 1).map(|c| !(c.is_alphanumeric() || c == '_')).unwrap_or(true))
330        && str.chars().nth(end).map(|c| !(c.is_alphanumeric() || c == '_')).unwrap_or(true)
331}
332
333fn replace<'a>(line: &'a str, replacements: &Vec<(String, Cow<str>)>) -> Cow<'a, str> {
334    let mut out: Cow<str> = line.into();
335
336    for replacement in replacements {
337        out = replace_all(out, &replacement.0, replacement.1.as_ref(), is_ident);
338    }
339
340    return out;
341}
342
343fn fn_replace<'a>(line: Cow<'a, str>, replacements: &Vec<(String, Vec<String>, String)>) -> Result<Cow<'a, str>> {
344    let mut out: Cow<str> = line;
345
346    for replacement in replacements {
347        out = replace_all_fn(out, replacement.0.as_str(), replacement.2.as_str(), &replacement.1, is_ident)?;
348    }
349
350    return Ok(out);
351}
352
353fn replace_all<'a>(str: Cow<'a, str>, to_match: &str, replacement: &str, predicate: impl Fn(&str, usize, usize) -> bool) -> Cow<'a, str> {
354    let matches = str.match_indices(to_match).collect::<Vec<_>>();
355
356    let mut out: Option<Cow<str>> = None;
357    let mut end_idx = str.len();
358
359    for (idx, _) in matches.into_iter().rev() {
360        if predicate(str.as_ref(), idx, idx + to_match.len()) {
361            let following_str = &str[idx + to_match.len()..end_idx];
362            end_idx = idx;
363            out = out.map(|m| concat_string!(replacement, following_str, m.as_ref()).into())
364                .or(Some(concat_string!(replacement, following_str).into()));
365        }
366    }
367
368    if end_idx != 0 {
369        out = out.map(|m| concat_string!(&str[0..end_idx], m.as_ref()).into())
370    }
371
372    return out.unwrap_or(str);
373}
374
375fn replace_all_fn<'a>(
376    str: Cow<'a, str>,
377    name: &str,
378    replacement: &str,
379    param_names: &Vec<String>,
380    predicate: impl Fn(&str, usize, usize) -> bool
381) -> Result<Cow<'a, str>> {
382    let matches = str.match_indices(name).collect::<Vec<_>>();
383
384    let mut out: Option<Cow<str>> = None;
385    let mut end_idx = str.len();
386
387    for (idx, _) in matches.into_iter().rev() {
388        let mut iter = str.chars();
389        if iter.by_ref().nth(idx + name.len()) != Some('(') {
390            continue;
391        }
392
393        let mut open = 1;
394        let mut params = Vec::new();
395        let mut cur = String::new();
396        let mut param_len = 0;
397        for (i, c) in iter.enumerate() {
398            if c == '(' {
399                open += 1
400            } else if c == ')' {
401                open -= 1;
402            }
403
404            if open == 0 {
405                param_len = i;
406                if cur.len() != 0 {
407                    params.push(cur);
408                }
409                break;
410            } else if open == 1 {
411                if c == ',' {
412                    params.push(cur);
413                    cur = String::new();
414                } else {
415                    cur.push(c);
416                }
417            }
418        }
419
420        let to_replace_len = name.len() + 2 + param_len;
421
422        if !predicate(str.as_ref(), idx, idx + to_replace_len) {
423            continue;
424        }
425
426        if params.len() != param_names.len() {
427            return Err(Error::NotEnoughParametersMacro(name.to_string()));
428        }
429
430        let params = param_names.iter()
431            .zip(params.iter());
432
433        let mut replacement = Cow::Borrowed(replacement);
434        for param in params {
435            replacement = replace_all(replacement, param.0, param.1, is_ident);
436        }
437
438        let following_str = &str[idx + to_replace_len..end_idx];
439        end_idx = idx;
440        out = out.map(|m| concat_string!(replacement, following_str, m.as_ref()).into())
441            .or(Some(concat_string!(replacement, following_str).into()));
442    }
443
444    if end_idx != 0 {
445        out = out.map(|m| concat_string!(&str[0..end_idx], m.as_ref()).into());
446    }
447
448    return Ok(out.unwrap_or(str));
449}