txtpp/core/execute/pp/
mod.rs

1use crate::core::{Mode, TagState};
2use crate::error::{PpError, PpErrorKind};
3use crate::fs::{AbsPath, IOCtx, Shell, TxtppPath};
4use error_stack::{Report, Result, ResultExt};
5use std::path::PathBuf;
6
7mod directive;
8pub use directive::*;
9
10/// Preprocess the txtpp file
11pub fn preprocess(
12    shell: &Shell,
13    input_file: &AbsPath,
14    mode: Mode,
15    is_first_pass: bool,
16    trailing_newline: bool,
17) -> Result<PpResult, PpError> {
18    Pp::run(input_file, shell, mode, is_first_pass, trailing_newline)
19}
20
21/// Preprocesser runtime
22struct Pp<'a> {
23    shell: &'a Shell,
24    input_file: AbsPath,
25    mode: Mode,
26    context: IOCtx,
27    cur_directive: Option<Directive>,
28    tag_state: TagState,
29    pp_mode: PpMode,
30    execute_tail_line: Option<String>,
31}
32
33impl<'a> Pp<'a> {
34    fn run(
35        input_file: &AbsPath,
36        shell: &'a Shell,
37        mode: Mode,
38        is_first_pass: bool,
39        trailing_newline: bool,
40    ) -> Result<PpResult, PpError> {
41        let context = IOCtx::new(input_file, mode.clone())?;
42        Self {
43            shell,
44            input_file: input_file.clone(),
45            mode,
46            context,
47            cur_directive: None,
48            tag_state: TagState::new(),
49            pp_mode: if is_first_pass {
50                PpMode::FirstPassExecute
51            } else {
52                PpMode::Execute
53            },
54            execute_tail_line: None,
55        }
56        .run_internal(trailing_newline)
57    }
58
59    fn run_internal(mut self, trailing_newline: bool) -> Result<PpResult, PpError> {
60        let mut add_newline_before_next_output = false;
61        // read txtpp file line by line
62        loop {
63            let line = self.get_next_line()?;
64
65            let (to_write, has_tail) = match self
66                .iterate_directive(line)
67                .ignore_err_if_cleaning(&self.mode, || IterDirectiveResult::None("".to_string()))?
68            {
69                IterDirectiveResult::Break => break,
70                IterDirectiveResult::LineTaken => {
71                    // Don't write the line
72                    (None, false)
73                }
74                IterDirectiveResult::None(line) => {
75                    // Writing the line from source to output
76                    let line = if self.pp_mode.is_execute() {
77                        self.tag_state.inject_tags(&line, self.context.line_ending)
78                    } else {
79                        line
80                    };
81                    (Some(line), false)
82                }
83                IterDirectiveResult::Execute(d, line) => {
84                    let whitespaces = d.whitespaces.clone();
85                    let d_str = format!("for `{d}`");
86                    let directive_output = if let Some(raw_output) = self
87                        .execute_directive(d)
88                        .map_err(|e| e.attach_printable(d_str))?
89                    {
90                        log::debug!("directive output: {raw_output:?}");
91                        if self.tag_state.try_store(&raw_output).is_err() {
92                            Some(self.format_directive_output(
93                                &whitespaces,
94                                raw_output.lines(),
95                                raw_output.ends_with('\n'),
96                            ))
97                        } else {
98                            None
99                        }
100                    } else {
101                        None
102                    };
103                    let has_tail = if line.is_some() {
104                        self.execute_tail_line = line;
105                        true
106                    } else {
107                        false
108                    };
109
110                    (directive_output, has_tail)
111                }
112            };
113
114            if self.pp_mode.is_execute() {
115                if let Some(x) = to_write {
116                    if add_newline_before_next_output {
117                        self.context.write_output(self.context.line_ending)?;
118                    }
119                    add_newline_before_next_output = !has_tail;
120                    self.context.write_output(&x)?;
121                }
122            }
123        }
124
125        if let PpMode::CollectDeps(deps) = self.pp_mode {
126            return Ok(PpResult::HasDeps(self.input_file, deps));
127        }
128
129        if self.tag_state.has_tags() && !matches!(self.mode, Mode::Clean) {
130            return Err(
131                Report::from(self.context.make_error(PpErrorKind::Directive))
132                    .attach_printable("Unused tag(s) found at the end of the file. Please make sure all created tags are used up.")
133                    .attach_printable(format!("tags: {}", self.tag_state))
134            );
135        }
136
137        if add_newline_before_next_output && trailing_newline {
138            self.context.write_output(self.context.line_ending)?;
139        }
140
141        self.context.done()?;
142
143        Ok(PpResult::Ok(self.input_file))
144    }
145
146    /// retrieve the next line
147    fn get_next_line(&mut self) -> Result<Option<String>, PpError> {
148        if self.execute_tail_line.is_some() {
149            return Ok(self.execute_tail_line.take());
150        }
151        let line = match self.context.next_line() {
152            Some(line) => Some(line?),
153            None => None,
154        };
155        Ok(line)
156    }
157
158    /// Update the directive and line based on the current directive and the next line
159    fn iterate_directive(&mut self, line: Option<String>) -> Result<IterDirectiveResult, PpError> {
160        let next = match line {
161            None => {
162                // End of file, execute the current directive
163                match self.cur_directive.take() {
164                    Some(d) => IterDirectiveResult::Execute(d, None),
165                    None => IterDirectiveResult::Break,
166                }
167            }
168            Some(line) => match self.cur_directive.take() {
169                // Detect new directive
170                None => match Directive::detect_from(&line) {
171                        Some(d) => {
172                            // make sure multi-line directives don't have empty prefix
173                            if d.directive_type.supports_multi_line() && d.prefix.is_empty() {
174                                return Err(
175                                    Report::from(self.context.make_error(PpErrorKind::Directive))
176                                        .attach_printable("multi-line directive must have a prefix. Trying adding a non-empty string before `TXTPP#`")
177                                );
178                            }
179                            // Detected, remove this line
180                            self.cur_directive = Some(d);
181                            IterDirectiveResult::LineTaken
182                        },
183                        None => {
184                            // Not detected, keep this line
185                            IterDirectiveResult::None(line)
186                        }
187                    }
188                ,
189                // Append to current directive
190                Some(mut d) => match d.add_line(&line) {
191                    Ok(_) => {
192                        // Added, remove this line
193                        self.cur_directive = Some(d);
194                        IterDirectiveResult::LineTaken
195                    },
196                    Err(_) => {
197                        // Not added, keep this line, and ready to execute the directive
198                        IterDirectiveResult::Execute(d, Some(line))
199                    }
200                },
201            },
202        };
203
204        log::debug!("next directive: {:?}", next);
205        Ok(next)
206    }
207
208    /// Execute the directive and return the output from the directive
209    fn execute_directive(&mut self, d: Directive) -> Result<Option<String>, PpError> {
210        if let Mode::Clean = self.mode {
211            // Ignore error if in clean mode
212            let _ = self.execute_in_clean_mode(d);
213            return Ok(None);
214        }
215        let d = match self.execute_in_collect_deps_mode(d)? {
216            Some(d) => d,
217            None => return Ok(None),
218        };
219
220        let raw_output = match d.directive_type {
221            DirectiveType::Empty | DirectiveType::After => {
222                // do nothing (consume the line)
223                None
224            }
225            DirectiveType::Run => {
226                let command = d.args.join(" ");
227                let output = self
228                    .shell
229                    .run(&command, &self.context.work_dir, &self.context.input_path)
230                    .map_err(|e| {
231                        e.change_context(self.context.make_error(PpErrorKind::Directive))
232                            .attach_printable(format!("failed to run command: `{command}`."))
233                    })?;
234                Some(output)
235            }
236            DirectiveType::Include => {
237                let arg = d.args.into_iter().next().unwrap_or_default();
238                let include_file = self
239                    .context
240                    .work_dir
241                    .try_resolve(&arg, false)
242                    .map_err(|e| {
243                        e.change_context(self.context.make_error(PpErrorKind::Directive))
244                            .attach_printable(format!("could not open include file: `{arg}`"))
245                    })?;
246                let output = std::fs::read_to_string(&include_file)
247                    .change_context_lazy(|| self.context.make_error(PpErrorKind::Directive))
248                    .attach_printable_lazy(|| {
249                        format!("could not read include file: `{include_file}`")
250                    })?;
251                log::debug!("include file content: {output:?}");
252                Some(output)
253            }
254            DirectiveType::Temp => {
255                self.execute_directive_temp(d.args, false)?;
256
257                None
258            }
259            DirectiveType::Tag => {
260                let tag_name = d.args.into_iter().next().unwrap_or_default();
261                self.tag_state.create(&tag_name).map_err(|e| {
262                    e.change_context(self.context.make_error(PpErrorKind::Directive))
263                        .attach_printable(format!("could not create tag: `{tag_name}`"))
264                })?;
265                None
266            }
267            DirectiveType::Write => Some(d.args.join("\n")),
268        };
269        Ok(raw_output)
270    }
271
272    /// Execute the directive in clean mode
273    fn execute_in_clean_mode(&mut self, d: Directive) -> Result<(), PpError> {
274        if let DirectiveType::Temp = d.directive_type {
275            self.execute_directive_temp(d.args, true)?;
276        }
277        Ok(())
278    }
279
280    /// Execute the directive in collect dep mode
281    fn execute_in_collect_deps_mode(&mut self, d: Directive) -> Result<Option<Directive>, PpError> {
282        if let PpMode::Execute = self.pp_mode {
283            // never collect deps in execute mode
284            return Ok(Some(d));
285        }
286        if matches!(
287            d.directive_type,
288            DirectiveType::Include | DirectiveType::After
289        ) {
290            let arg = d.args.first().cloned().unwrap_or_default();
291            let include_path = PathBuf::from(&arg);
292            // We use join instead of share_base because the dependency might not exist
293            let include_path = self.context.work_dir.as_path().join(include_path);
294            // See if we need to store the dependency and come back later
295            if let Some(x) = include_path.get_txtpp_file() {
296                log::debug!("found dependency: {}", x.display());
297                let p_abs = self.context.work_dir.share_base(x).map_err(|e| {
298                    e.change_context(self.context.make_error(PpErrorKind::Directive))
299                        .attach_printable(format!(
300                            "could not resolve include file: `{}`",
301                            include_path.display()
302                        ))
303                })?;
304                match &mut self.pp_mode {
305                    PpMode::CollectDeps(deps) => {
306                        deps.push(p_abs);
307                    }
308                    PpMode::FirstPassExecute => {
309                        self.pp_mode = PpMode::CollectDeps(vec![p_abs]);
310                    }
311                    _ => unreachable!(),
312                }
313                return Ok(None);
314            }
315        }
316        // if we are already collecting deps, don't execute the directive
317        if let PpMode::CollectDeps(_) = self.pp_mode {
318            return Ok(None);
319        }
320        Ok(Some(d))
321    }
322
323    fn execute_directive_temp(&mut self, args: Vec<String>, is_clean: bool) -> Result<(), PpError> {
324        let export_file = match args.first() {
325            Some(p) => p,
326            None => {
327                return Err(Report::new(self.context.make_error(PpErrorKind::Directive))
328                    .attach_printable("invalid temp directive: no export file path specified"));
329            }
330        };
331
332        if PathBuf::from(export_file).is_txtpp_file() {
333            return Err(Report::new(self.context.make_error(PpErrorKind::Directive))
334                .attach_printable(format!(
335                "invalid temp directive: export file path cannot be a txtpp file: `{export_file}`"
336            )));
337        }
338
339        if is_clean {
340            return self.context.write_temp_file(export_file, "");
341        }
342        // We force trailing newline if the file is not empty
343        let contents = self.format_directive_output("", args.iter().skip(1), false);
344        self.context.write_temp_file(export_file, &contents)
345    }
346
347    fn format_directive_output(
348        &mut self,
349        whitespaces: &str,
350        raw_output: impl Iterator<Item = impl AsRef<str>>,
351        has_trailing_newline: bool,
352    ) -> String {
353        let mut output = raw_output
354            .map(|s| format!("{whitespaces}{line}", line = s.as_ref()))
355            .collect::<Vec<_>>()
356            .join(self.context.line_ending);
357        if has_trailing_newline {
358            output.push_str(self.context.line_ending);
359        }
360        output
361    }
362}
363
364trait IgnoreIfCleaning {
365    type Output;
366    fn ignore_err_if_cleaning<F>(self, mode: &Mode, f: F) -> Result<Self::Output, PpError>
367    where
368        Self: Sized,
369        F: FnOnce() -> Self::Output;
370}
371
372impl<T> IgnoreIfCleaning for Result<T, PpError> {
373    type Output = T;
374    fn ignore_err_if_cleaning<F>(self, mode: &Mode, f: F) -> Result<T, PpError>
375    where
376        F: FnOnce() -> Self::Output,
377    {
378        if self.is_err() && matches!(mode, Mode::Clean) {
379            Ok(f())
380        } else {
381            self
382        }
383    }
384}
385
386/// Result of reading the next line of a directive
387#[derive(Debug)]
388enum IterDirectiveResult {
389    /// Stop processing
390    Break,
391    /// The directive is none and the line is not a directive
392    None(String),
393    /// The next line is taken by the directive
394    LineTaken,
395    /// The directive is complete and should be executed
396    Execute(Directive, Option<String>),
397}
398enum PpMode {
399    /// Execute until the first dep, and turn into `CollectDeps`
400    FirstPassExecute,
401    /// Execute, don't collect deps
402    Execute,
403    /// Just collect deps
404    CollectDeps(Vec<AbsPath>),
405}
406
407impl PpMode {
408    fn is_execute(&self) -> bool {
409        matches!(self, PpMode::Execute | PpMode::FirstPassExecute)
410    }
411}
412
413/// Processing result
414#[derive(Debug)]
415pub enum PpResult {
416    /// File was processed successfully
417    Ok(AbsPath),
418    /// Dependency is found
419    HasDeps(AbsPath, Vec<AbsPath>),
420}