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
30pub 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
47pub 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
66pub 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
93pub 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}