yolk/templating/
element.rs

1use crate::script::eval_ctx::EvalCtx;
2use miette::Result;
3
4use super::{comment_style::CommentStyle, error::TemplateError, parser::Sp};
5
6/// A single, full line with a tag in it. Contains the span of the entire line.
7#[derive(Debug, Eq, PartialEq, arbitrary::Arbitrary)]
8pub struct TaggedLine<'a> {
9    pub left: &'a str,
10    pub tag: &'a str,
11    pub right: &'a str,
12    pub full_line: Sp<&'a str>,
13}
14
15/// The starting line and body of a block, such as a multiline tag or part of a conditional.
16///
17/// `Expr` should either be `Sp<&'a str>` or `()`.
18#[derive(Debug, Eq, PartialEq, arbitrary::Arbitrary)]
19pub struct Block<'a, Expr = Sp<&'a str>> {
20    /// The full line including the tag
21    pub tagged_line: TaggedLine<'a>,
22    pub expr: Expr,
23    pub body: Vec<Element<'a>>,
24}
25
26impl<'a, Expr> Block<'a, Expr> {
27    pub fn map_expr<T>(self, f: impl FnOnce(Expr) -> T) -> Block<'a, T> {
28        Block {
29            tagged_line: self.tagged_line,
30            expr: f(self.expr),
31            body: self.body,
32        }
33    }
34}
35
36#[derive(Debug, Eq, PartialEq, arbitrary::Arbitrary)]
37pub enum Element<'a> {
38    Plain(Sp<&'a str>),
39    Inline {
40        /// The full line including the tag
41        line: TaggedLine<'a>,
42        expr: Sp<&'a str>,
43        is_if: bool,
44    },
45    NextLine {
46        /// The full line including the tag
47        tagged_line: TaggedLine<'a>,
48        expr: Sp<&'a str>,
49        next_line: Sp<&'a str>,
50        is_if: bool,
51        full_span: Sp<&'a str>,
52    },
53    MultiLine {
54        block: Block<'a, Sp<&'a str>>,
55        end: TaggedLine<'a>,
56        full_span: Sp<&'a str>,
57    },
58    Conditional {
59        blocks: Vec<Block<'a, Sp<&'a str>>>,
60        else_block: Option<Block<'a, ()>>,
61        end: TaggedLine<'a>,
62        full_span: Sp<&'a str>,
63    },
64}
65
66impl<'a> Element<'a> {
67    #[allow(unused)]
68    pub fn try_from_str(s: &'a str) -> Result<Self> {
69        use crate::templating::parser;
70        use miette::IntoDiagnostic as _;
71        parser::parse_element(s).into_diagnostic()
72    }
73
74    pub fn full_span(&self) -> &Sp<&str> {
75        match self {
76            Element::Plain(sp) => &sp,
77            Element::Inline { line, .. } => &line.full_line,
78            Element::NextLine { full_span, .. } => full_span,
79            Element::MultiLine { full_span, .. } => full_span,
80            Element::Conditional { full_span, .. } => full_span,
81        }
82    }
83
84    pub fn render(
85        &self,
86        comment_style: &CommentStyle,
87        eval_ctx: &mut EvalCtx,
88    ) -> Result<String, TemplateError> {
89        match self {
90            Element::Plain(s) => Ok(s.as_str().to_string()),
91            Element::Inline { line, expr, is_if } => match is_if {
92                true => {
93                    let eval_result = eval_ctx
94                        .eval_rhai::<bool>(expr.as_str())
95                        .map_err(|e| TemplateError::from_rhai(e, expr.range()))?;
96                    Ok(comment_style.toggle_string(line.full_line.as_str(), eval_result))
97                }
98                false => Ok(format!(
99                    "{}{}{}",
100                    run_transformation_expr(eval_ctx, line.left, expr)?,
101                    line.tag,
102                    line.right
103                )),
104            },
105            Element::NextLine {
106                tagged_line: line,
107                expr,
108                next_line,
109                is_if,
110                ..
111            } => match is_if {
112                true => Ok(format!(
113                    "{}{}",
114                    line.full_line.as_str(),
115                    &comment_style.toggle_string(
116                        next_line.as_str(),
117                        eval_ctx
118                            .eval_rhai::<bool>(expr.as_str())
119                            .map_err(|e| TemplateError::from_rhai(e, expr.range()))?
120                    )
121                )),
122                false => Ok(format!(
123                    "{}{}",
124                    line.full_line.as_str(),
125                    run_transformation_expr(eval_ctx, next_line.as_str(), expr)?
126                )),
127            },
128            Element::MultiLine { block, end, .. } => {
129                let rendered_body = render_elements(comment_style, eval_ctx, &block.body)?;
130                Ok(format!(
131                    "{}{}{}",
132                    block.tagged_line.full_line.as_str(),
133                    &run_transformation_expr(eval_ctx, &rendered_body, &block.expr)?,
134                    end.full_line.as_str(),
135                ))
136            }
137            Element::Conditional {
138                blocks,
139                else_block,
140                end,
141                ..
142            } => {
143                let mut output = String::new();
144                let mut had_true = false;
145                for block in blocks {
146                    // If we've already had a true block, we want to return false for every other one.
147                    // If we haven't, and there's an expression, evaluate it.
148                    // If there isn't, we're on the else block, which should be true iff we haven't had a true block yet.
149                    let expr_true = !had_true
150                        && eval_ctx
151                            .eval_rhai::<bool>(block.expr.as_str())
152                            .map_err(|e| TemplateError::from_rhai(e, block.expr.range()))?;
153                    had_true = had_true || expr_true;
154
155                    let rendered_body = if expr_true {
156                        render_elements(comment_style, eval_ctx, &block.body)?
157                    } else {
158                        render_no_eval(&block.body)
159                    };
160                    output.push_str(block.tagged_line.full_line.as_str());
161                    output.push_str(&comment_style.toggle_string(&rendered_body, expr_true));
162                }
163                if let Some(block) = else_block {
164                    let expr_true = !had_true;
165                    let rendered_body = render_elements(comment_style, eval_ctx, &block.body)?;
166                    output.push_str(block.tagged_line.full_line.as_str());
167                    output.push_str(&comment_style.toggle_string(&rendered_body, expr_true));
168                }
169                output.push_str(end.full_line.as_str());
170                Ok(output)
171            }
172        }
173    }
174}
175
176pub fn render_elements(
177    comment_style: &CommentStyle,
178    eval_ctx: &mut EvalCtx,
179    elements: &[Element<'_>],
180) -> Result<String, TemplateError> {
181    let mut errs = Vec::new();
182    let mut output = String::new();
183    for element in elements {
184        match element.render(comment_style, eval_ctx) {
185            Ok(rendered) => output.push_str(&rendered),
186            Err(e) => errs.push(e),
187        }
188    }
189    if errs.is_empty() {
190        Ok(output)
191    } else {
192        Err(TemplateError::Multiple(errs))
193    }
194}
195
196pub fn render_no_eval(elements: &[Element<'_>]) -> String {
197    elements.iter().map(|x| x.full_span().as_str()).collect()
198}
199
200fn run_transformation_expr(
201    eval_ctx: &mut EvalCtx,
202    text: &str,
203    expr: &Sp<&str>,
204) -> Result<String, TemplateError> {
205    let result = eval_ctx
206        .eval_text_transformation(text, expr.as_str())
207        .map_err(|e| TemplateError::from_rhai(e, expr.range()))?;
208    let second_pass = eval_ctx
209        .eval_text_transformation(&result, expr.as_str())
210        .map_err(|e| TemplateError::from_rhai(e, expr.range()))?;
211    if result != second_pass {
212        cov_mark::hit!(refuse_nonidempodent_transformation);
213        println!(
214            "Warning: Refusing to apply transformation that is not idempodent: `{}`",
215            expr.as_str()
216        );
217        Ok(text.to_string())
218    } else {
219        Ok(result)
220    }
221}