ppx_impl/
lib.rs

1use std::borrow::Cow;
2use std::path::{Path, PathBuf};
3
4use concat_string::concat_string;
5use itertools::Itertools;
6use thiserror::Error;
7
8#[derive(Error, Debug)]
9pub enum Error {
10    #[error("Invalid macro `{}` on line {}", .0, .1)]
11    InvalidMacro(String, usize),
12    #[error("Found extra parameters in #param macro on line {}", .0)]
13    ExtraParamsInParamMacro(usize),
14    #[error("Not enough parameters passed to template file")]
15    NotEnoughParameters,
16    #[error("Not enough parameters passed to function-like macro `{}`", .0)]
17    NotEnoughParametersMacro(String),
18    #[error("Invalid parameter name {} on line {}", .0, .1)]
19    InvalidParameterName(String, usize),
20    #[error("First parameter of #include should be a string on line {}", .0)]
21    FirstParamOfIncludeNotString(usize),
22    #[error("Unused parameters while expanding macro file")]
23    UnusedParameters,
24    #[error("IOError while reading {}: {}", .1.display(), .0)]
25    IOError(std::io::Error, PathBuf)
26}
27
28type Result<T> = std::result::Result<T, Error>;
29
30/// Parses a file using the templating engine.
31///
32/// For an example, see [parse_string].
33///
34/// # Parameters
35/// - `input_file`: the file that is read
36/// - `base_dir`: all includes are resolved relative to this directory
37/// - `parameters`: if `input_file` contains any parameter macros, pass an iterator
38///                 to them here. Otherwise pass `std::iter::empty()`.
39pub fn parse<'a>(
40    input_file: impl AsRef<Path>,
41    base_dir: impl AsRef<Path>,
42    parameters: impl Iterator<Item = &'a str>,
43) -> Result<String> {
44    return parse_cow(input_file, base_dir, parameters);
45}
46
47/// Same as [parse], but the parameters iterator has an item of `impl Into<Cow<str>>`.
48/// This is so that an empty iterator can be passed to `parse`.
49pub fn parse_cow<'a, Iter, C>(
50    input_file: impl AsRef<Path>,
51    base_dir: impl AsRef<Path>,
52    parameters: Iter
53) -> Result<String>
54    where
55        Iter: Iterator<Item = C>,
56        C: Into<Cow<'a, str>>
57{
58    let content = match std::fs::read_to_string(input_file.as_ref()) {
59        Ok(content) => content,
60        Err(e) => return Err(Error::IOError(e, input_file.as_ref().to_path_buf())),
61    };
62
63    return parse_string_cow(&content, base_dir, parameters);
64}
65
66/// Parses a file using the templating engine.
67///
68/// # Parameters
69/// - `input`: the contents to process
70/// - `base_dir`: all includes are resolved relative to this directory
71/// - `parameters`: if `input` contains any parameter macros, pass an iterator
72///                 to them here. Otherwise pass `std::iter::empty()`.
73///
74/// # Example
75///
76/// ```rust
77/// # use ppx_impl::*;
78/// let res = parse_string(
79///     "#define A 4\nThe answer is A",
80///     std::env::current_dir().unwrap(),
81///     std::iter::empty()
82/// ).unwrap();
83/// assert_eq!(res, "The answer is 4");
84/// ```
85pub fn parse_string<'a>(
86    input: &str,
87    base_dir: impl AsRef<Path>,
88    parameters: impl Iterator<Item = &'a str>
89) -> Result<String> {
90    parse_string_cow(input, base_dir, parameters)
91}
92
93/// Same as [parse_string], but the parameters iterator has an item of `impl Into<Cow<str>>`.
94/// This is so that an empty iterator can be passed to `parse_string`.
95pub fn parse_string_cow<'a, Iter, C>(
96    input: &str,
97    base_dir: impl AsRef<Path>,
98    parameters: Iter
99) -> Result<String>
100    where
101        Iter: Iterator<Item = C>,
102        C: Into<Cow<'a, str>>
103{
104    parse_string_cow_impl(input, base_dir.as_ref(), &mut parameters.map(|v| v.into()))
105}
106
107fn parse_string_cow_impl<'a>(
108    input: &str,
109    base_dir: &Path,
110    parameters: &mut dyn Iterator<Item = Cow<'a, str>>
111) -> Result<String> {
112    let mut out = String::new();
113
114    let mut replacements: Vec<(String, Cow<str>)> = vec![];
115    let mut fn_replacements: Vec<(String, Vec<String>, String)> = vec![];
116    let mut cur_fn_replacement: Option<(String, Vec<String>, String)> = None;
117
118    let max_lines = input.chars()
119        .filter(|c| *c == '\n')
120        .count();
121
122    for (line_num, line) in input.lines().enumerate() {
123        if let Some(cur_fn_repl) = cur_fn_replacement {
124            if line.ends_with("\\") {
125                cur_fn_replacement = Some((cur_fn_repl.0, cur_fn_repl.1, cur_fn_repl.2 + &line[..line.len()-1]))
126            } else {
127                fn_replacements.push((cur_fn_repl.0, cur_fn_repl.1, cur_fn_repl.2 + line));
128                cur_fn_replacement = None;
129            }
130            continue;
131        }
132
133        let mut line_chars = line.chars().skip_while(char::is_ascii_whitespace);
134        let start_char = line_chars.by_ref().next();
135        match start_char {
136            Some('#') => {
137                let macro_name = line_chars.by_ref().take_while(|c| c.is_ascii_alphanumeric()).collect::<String>();
138                match macro_name.as_str() {
139                    "define" => {
140                        let mut is_last_bracket = false;
141                        let name = line_chars.by_ref()
142                            .skip_while(char::is_ascii_whitespace)
143                            .take_while(|c| {
144                                if *c == '(' {
145                                    is_last_bracket = true;
146                                }
147                                !c.is_ascii_whitespace() && *c != '('
148                            })
149                            .collect::<String>();
150
151                        if is_last_bracket {
152                            let params = line_chars.by_ref()
153                                .take_while(|c| *c != ')')
154                                .chunk_by(|c| *c == ',');
155                            let params = params
156                                .into_iter()
157                                .filter(|(b, _)| !b)
158                                .map(|(_, i)| i
159                                    .skip_while(char::is_ascii_whitespace)
160                                    .take_while(|c| !c.is_ascii_whitespace())
161                                    .collect::<String>())
162                                .collect::<Vec<String>>();
163
164                            let check_param_name = params.iter().find(|c| !c.chars().all(|c| c.is_alphanumeric()));
165                            if let Some(param_name) = check_param_name {
166                                return Err(Error::InvalidParameterName(param_name.clone(), line_num))
167                            }
168
169                            let replacement = line_chars.by_ref().collect::<String>();
170
171                            if replacement.ends_with("\\") {
172                                cur_fn_replacement = Some((name, params, replacement[..replacement.len()-1].to_string()));
173                            } else {
174                                fn_replacements.push((name, params, replacement));
175                            }
176                        } else {
177                            let replacement = line_chars.collect::<Cow<str>>();
178                            replacements.push((name, replacement))
179                        }
180                    }, "include" => {
181                        let path = line_chars.by_ref()
182                            .skip_while(char::is_ascii_whitespace)
183                            .take_while(|c| !c.is_ascii_whitespace())
184                            .collect::<String>();
185
186                        if !(path.starts_with('"') && path.ends_with('"')) {
187                            return Err(Error::FirstParamOfIncludeNotString(line_num));
188                        }
189
190                        let path = &path[1..path.len()-1];
191
192                        let params = line_chars.by_ref()
193                            .chunk_by(|c| *c == ',');
194                        let mut params = params
195                            .into_iter()
196                            .filter(|(b, _)| !b)
197                            .map(|(_, i)| Cow::Owned(i.collect::<String>()));
198
199                        let file_path = base_dir.join(path);
200                        let content = match std::fs::read_to_string(&file_path) {
201                            Ok(content) => content,
202                            Err(e) => return Err(Error::IOError(e, file_path)),
203                        };
204
205                        out += parse_string_cow_impl(&content, base_dir.as_ref(), &mut params)?.as_str();
206                    }, "param" => {
207                        let param_name = line_chars.by_ref()
208                            .skip_while(|c| c.is_ascii_whitespace())
209                            .take_while(|c| !c.is_ascii_whitespace())
210                            .collect::<String>();
211
212                        if !line_chars.by_ref().all(|c| c.is_ascii_whitespace()) {
213                            return Err(Error::ExtraParamsInParamMacro(line_num));
214                        }
215
216                        let Some(param_value) = parameters.next() else {
217                            return Err(Error::NotEnoughParameters);
218                        };
219
220                        replacements.push((param_name, param_value.into()));
221                    },
222                    _ => return Err(Error::InvalidMacro(macro_name, line_num)),
223                }
224            },
225            Some('\\') if (line_chars.next() == Some('#')) => {
226                out += fn_replace(replace(&line.replacen("\\#", "#", 1), &replacements), &fn_replacements)?.as_ref();
227                if line_num != max_lines {
228                    out += "\n";
229                }
230            },
231            _ => {
232                out += fn_replace(replace(line, &replacements), &fn_replacements)?.as_ref();
233                if line_num != max_lines {
234                    out += "\n";
235                }
236            },
237        }
238    }
239
240    if parameters.count() != 0 {
241        return Err(Error::UnusedParameters);
242    }
243
244    return Ok(out);
245}
246
247fn is_ident(str: &str, start: usize, end: usize) -> bool {
248    (start == 0 || str.chars().nth(start - 1).map(|c| !c.is_alphanumeric()).unwrap_or(true))
249        && str.chars().nth(end).map(|c| !c.is_alphanumeric()).unwrap_or(true)
250}
251
252fn replace<'a>(line: &'a str, replacements: &Vec<(String, Cow<str>)>) -> Cow<'a, str> {
253    let mut out: Cow<str> = line.into();
254
255    for replacement in replacements {
256        out = replace_all(out, &replacement.0, replacement.1.as_ref(), is_ident);
257    }
258
259    return out;
260}
261
262fn fn_replace<'a>(line: Cow<'a, str>, replacements: &Vec<(String, Vec<String>, String)>) -> Result<Cow<'a, str>> {
263    let mut out: Cow<str> = line;
264
265    for replacement in replacements {
266        out = replace_all_fn(out, replacement.0.as_str(), replacement.2.as_str(), &replacement.1, is_ident)?;
267    }
268
269    return Ok(out);
270}
271
272fn replace_all<'a>(str: Cow<'a, str>, to_match: &str, replacement: &str, predicate: impl Fn(&str, usize, usize) -> bool) -> Cow<'a, str> {
273    let matches = str.match_indices(to_match).collect::<Vec<_>>();
274
275    let mut out: Option<Cow<str>> = None;
276    let mut end_idx = str.len();
277
278    for (idx, _) in matches.into_iter().rev() {
279        if predicate(str.as_ref(), idx, idx + to_match.len()) {
280            let following_str = &str[idx + to_match.len()..end_idx];
281            end_idx = idx;
282            out = out.map(|m| concat_string!(replacement, following_str, m.as_ref()).into())
283                .or(Some(concat_string!(replacement, following_str).into()));
284        }
285    }
286
287    if end_idx != 0 {
288        out = out.map(|m| concat_string!(&str[0..end_idx], m.as_ref()).into())
289    }
290
291    return out.unwrap_or(str);
292}
293
294fn replace_all_fn<'a>(
295    str: Cow<'a, str>,
296    name: &str,
297    replacement: &str,
298    param_names: &Vec<String>,
299    predicate: impl Fn(&str, usize, usize) -> bool
300) -> Result<Cow<'a, str>> {
301    let matches = str.match_indices(name).collect::<Vec<_>>();
302
303    let mut out: Option<Cow<str>> = None;
304    let mut end_idx = str.len();
305
306    for (idx, _) in matches.into_iter().rev() {
307        let mut iter = str.chars();
308        if iter.by_ref().nth(idx + name.len()) != Some('(') {
309            continue;
310        }
311
312        let mut open = 1;
313        let mut params = Vec::new();
314        let mut cur = String::new();
315        let mut param_len = 0;
316        for (i, c) in iter.enumerate() {
317            if c == '(' {
318                open += 1
319            } else if c == ')' {
320                open -= 1;
321            }
322
323            if open == 0 {
324                param_len = i;
325                if cur.len() != 0 {
326                    params.push(cur);
327                }
328                break;
329            } else if open == 1 {
330                if c == ',' {
331                    params.push(cur);
332                    cur = String::new();
333                } else {
334                    cur.push(c);
335                }
336            }
337        }
338
339        let to_replace_len = name.len() + 2 + param_len;
340
341        if !predicate(str.as_ref(), idx, idx + to_replace_len) {
342            continue;
343        }
344
345        if params.len() != param_names.len() {
346            return Err(Error::NotEnoughParametersMacro(name.to_string()));
347        }
348
349        let params = param_names.iter()
350            .zip(params.iter());
351
352        let mut replacement = Cow::Borrowed(replacement);
353        for param in params {
354            replacement = replace_all(replacement, param.0, param.1, is_ident);
355        }
356
357        let following_str = &str[idx + to_replace_len..end_idx];
358        end_idx = idx;
359        out = out.map(|m| concat_string!(replacement, following_str, m.as_ref()).into())
360            .or(Some(concat_string!(replacement, following_str).into()));
361    }
362
363    if end_idx != 0 {
364        out = out.map(|m| concat_string!(&str[0..end_idx], m.as_ref()).into());
365    }
366
367    return Ok(out.unwrap_or(str));
368}