tera/parser/
whitespace.rs

1use crate::parser::ast::*;
2
3macro_rules! trim_right_previous {
4    ($vec: expr) => {
5        if let Some(last) = $vec.pop() {
6            if let Node::Text(mut s) = last {
7                s = s.trim_end().to_string();
8                if !s.is_empty() {
9                    $vec.push(Node::Text(s));
10                }
11            } else {
12                $vec.push(last);
13            }
14        }
15    };
16    ($cond: expr, $vec: expr) => {
17        if $cond {
18            trim_right_previous!($vec);
19        }
20    };
21}
22
23/// Removes whitespace from the AST nodes according to the `{%-` and `-%}` defined in the template.
24/// Empty string nodes will be discarded.
25///
26/// The `ws` param is used when recursing through nested bodies to know whether to know
27/// how to handle the whitespace for that whole body:
28/// - set the initial `trim_left_next` to `ws.left`
29/// - trim last node if it is a text node if `ws.right == true`
30pub fn remove_whitespace(nodes: Vec<Node>, body_ws: Option<WS>) -> Vec<Node> {
31    let mut res = Vec::with_capacity(nodes.len());
32
33    // Whether the node we just added to res is a Text node
34    let mut previous_was_text = false;
35    // Whether the previous block ended wth `-%}` and we need to trim left the next text node
36    let mut trim_left_next = body_ws.map_or(false, |ws| ws.left);
37
38    for n in nodes {
39        match n {
40            Node::Text(s) => {
41                previous_was_text = true;
42
43                if !trim_left_next {
44                    res.push(Node::Text(s));
45                    continue;
46                }
47                trim_left_next = false;
48
49                let new_val = s.trim_start();
50                if !new_val.is_empty() {
51                    res.push(Node::Text(new_val.to_string()));
52                }
53                // empty text nodes will be skipped
54                continue;
55            }
56            Node::VariableBlock(ws, _)
57            | Node::ImportMacro(ws, _, _)
58            | Node::Extends(ws, _)
59            | Node::Include(ws, _, _)
60            | Node::Set(ws, _)
61            | Node::Break(ws)
62            | Node::Comment(ws, _)
63            | Node::Continue(ws) => {
64                trim_right_previous!(previous_was_text && ws.left, res);
65                trim_left_next = ws.right;
66            }
67            Node::Raw(start_ws, ref s, end_ws) => {
68                trim_right_previous!(previous_was_text && start_ws.left, res);
69                previous_was_text = false;
70                trim_left_next = end_ws.right;
71
72                if start_ws.right || end_ws.left {
73                    let val = if start_ws.right && end_ws.left {
74                        s.trim()
75                    } else if start_ws.right {
76                        s.trim_start()
77                    } else {
78                        s.trim_end()
79                    };
80
81                    res.push(Node::Raw(start_ws, val.to_string(), end_ws));
82                    continue;
83                }
84            }
85            // Those nodes have a body surrounded by 2 tags
86            Node::Forloop(start_ws, _, end_ws)
87            | Node::MacroDefinition(start_ws, _, end_ws)
88            | Node::FilterSection(start_ws, _, end_ws)
89            | Node::Block(start_ws, _, end_ws) => {
90                trim_right_previous!(previous_was_text && start_ws.left, res);
91                previous_was_text = false;
92                trim_left_next = end_ws.right;
93
94                // let's remove ws from the bodies now and append the cleaned up node
95                let body_ws = WS { left: start_ws.right, right: end_ws.left };
96                match n {
97                    Node::Forloop(_, mut forloop, _) => {
98                        forloop.body = remove_whitespace(forloop.body, Some(body_ws));
99                        res.push(Node::Forloop(start_ws, forloop, end_ws));
100                    }
101                    Node::MacroDefinition(_, mut macro_def, _) => {
102                        macro_def.body = remove_whitespace(macro_def.body, Some(body_ws));
103                        res.push(Node::MacroDefinition(start_ws, macro_def, end_ws));
104                    }
105                    Node::FilterSection(_, mut filter_section, _) => {
106                        filter_section.body = remove_whitespace(filter_section.body, Some(body_ws));
107                        res.push(Node::FilterSection(start_ws, filter_section, end_ws));
108                    }
109                    Node::Block(_, mut block, _) => {
110                        block.body = remove_whitespace(block.body, Some(body_ws));
111                        res.push(Node::Block(start_ws, block, end_ws));
112                    }
113                    _ => unreachable!(),
114                };
115                continue;
116            }
117            // The ugly one
118            Node::If(If { conditions, otherwise }, end_ws) => {
119                trim_left_next = end_ws.right;
120                let mut new_conditions: Vec<(_, _, Vec<_>)> = Vec::with_capacity(conditions.len());
121
122                for mut condition in conditions {
123                    if condition.0.left {
124                        // We need to trim the text node before the if tag
125                        if new_conditions.is_empty() && previous_was_text {
126                            trim_right_previous!(res);
127                        } else if let Some(&mut (_, _, ref mut body)) = new_conditions.last_mut() {
128                            trim_right_previous!(body);
129                        }
130                    }
131
132                    // we can't peek at the next one to know whether we need to trim right since
133                    // are consuming conditions. We'll find out at the next iteration.
134                    condition.2 = remove_whitespace(
135                        condition.2,
136                        Some(WS { left: condition.0.right, right: false }),
137                    );
138                    new_conditions.push(condition);
139                }
140
141                previous_was_text = false;
142
143                // We now need to look for the last potential `{%-` bit for if/elif
144
145                // That can be a `{%- else`
146                if let Some((else_ws, body)) = otherwise {
147                    if else_ws.left {
148                        if let Some(&mut (_, _, ref mut body)) = new_conditions.last_mut() {
149                            trim_right_previous!(body);
150                        }
151                    }
152                    let mut else_body =
153                        remove_whitespace(body, Some(WS { left: else_ws.right, right: false }));
154                    // if we have an `else`, the `endif` will affect the else node so we need to check
155                    if end_ws.left {
156                        trim_right_previous!(else_body);
157                    }
158                    res.push(Node::If(
159                        If { conditions: new_conditions, otherwise: Some((else_ws, else_body)) },
160                        end_ws,
161                    ));
162                    continue;
163                }
164
165                // Or `{%- endif`
166                if end_ws.left {
167                    if let Some(&mut (_, _, ref mut body)) = new_conditions.last_mut() {
168                        trim_right_previous!(true, body);
169                    }
170                }
171
172                res.push(Node::If(If { conditions: new_conditions, otherwise }, end_ws));
173                continue;
174            }
175            Node::Super => (),
176        };
177
178        // If we are there, that means it's not a text node and we didn't have to modify the node
179        previous_was_text = false;
180        res.push(n);
181    }
182
183    if let Some(whitespace) = body_ws {
184        trim_right_previous!(whitespace.right, res);
185    }
186
187    res
188}