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