1
/*
2
 * Hurl (https://hurl.dev)
3
 * Copyright (C) 2024 Orange
4
 *
5
 * Licensed under the Apache License, Version 2.0 (the "License");
6
 * you may not use this file except in compliance with the License.
7
 * You may obtain a copy of the License at
8
 *
9
 *          http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 *
17
 */
18
use crate::ast::{Placeholder, SourceInfo, TemplateElement};
19
use crate::parser::primitives::zero_or_more_spaces;
20
use crate::parser::{error, expr, ParseResult};
21
use crate::reader::{Pos, Reader};
22

            
23
pub struct EncodedString {
24
    pub source_info: SourceInfo,
25
    pub chars: Vec<(char, String, Pos)>,
26
}
27

            
28
46730
pub fn templatize(encoded_string: EncodedString) -> ParseResult<Vec<TemplateElement>> {
29
    enum State {
30
        String,
31
        Template,
32
        FirstOpenBracket,
33
        FirstCloseBracket,
34
    }
35

            
36
46730
    let mut elements = vec![];
37
46730

            
38
46730
    let mut value = String::new();
39
46730
    let mut encoded = String::new();
40
46730
    let mut state = State::String;
41
46730
    let mut expression_start = None;
42

            
43
1005870
    for (c, s, pos) in encoded_string.chars {
44
959145
        match state {
45
            State::String => {
46
949475
                if s.as_str() == "{" {
47
2390
                    state = State::FirstOpenBracket;
48
947085
                } else {
49
947085
                    value.push(c);
50
947085
                    encoded.push_str(&s.clone());
51
                }
52
            }
53

            
54
            State::FirstOpenBracket => {
55
2370
                if s.as_str() == "{" {
56
975
                    if !value.is_empty() {
57
200
                        elements.push(TemplateElement::String { value, encoded });
58
200
                        value = String::new();
59
200
                        encoded = String::new();
60
                    }
61
975
                    state = State::Template;
62
1395
                } else {
63
1395
                    value.push('{');
64
1395
                    encoded.push('{');
65
1395

            
66
1395
                    value.push(c);
67
1395
                    encoded.push_str(&s.clone());
68
1395
                    state = State::String;
69
                }
70
            }
71

            
72
            State::Template => {
73
6325
                if expression_start.is_none() {
74
975
                    expression_start = Some(pos);
75
                }
76
6325
                if s.as_str() == "}" {
77
975
                    state = State::FirstCloseBracket;
78
5350
                } else {
79
5350
                    value.push(c);
80
5350
                    encoded.push_str(&s.clone());
81
                }
82
            }
83

            
84
            State::FirstCloseBracket => {
85
975
                if s.as_str() == "}" {
86
975
                    let mut reader = Reader::with_pos(encoded.as_str(), expression_start.unwrap());
87
975
                    let space0 = zero_or_more_spaces(&mut reader)?;
88
975
                    let expr = expr::parse(&mut reader)?;
89
970
                    let space1 = zero_or_more_spaces(&mut reader)?;
90
970
                    let placeholder = Placeholder {
91
970
                        space0,
92
970
                        expr,
93
970
                        space1,
94
970
                    };
95
970
                    elements.push(TemplateElement::Placeholder(placeholder));
96
970
                    value = String::new();
97
970
                    encoded = String::new();
98
970
                    expression_start = None;
99
970
                    state = State::String;
100
                } else {
101
                    value.push('}');
102
                    value.push(c);
103
                    encoded.push('}');
104
                    encoded.push_str(&s.clone());
105
                }
106
            }
107
        }
108
    }
109

            
110
46725
    match state {
111
46705
        State::String => {}
112
20
        State::FirstOpenBracket => {
113
20
            value.push('{');
114
20
            encoded.push('{');
115
        }
116
        State::Template | State::FirstCloseBracket => {
117
            let kind = error::ParseErrorKind::Expecting {
118
                value: "}}".to_string(),
119
            };
120
            return Err(error::ParseError::new(
121
                encoded_string.source_info.end,
122
                false,
123
                kind,
124
            ));
125
        }
126
    }
127

            
128
46725
    if !value.is_empty() {
129
45650
        elements.push(TemplateElement::String { value, encoded });
130
    }
131
46725
    Ok(elements)
132
}
133

            
134
#[cfg(test)]
135
mod tests {
136

            
137
    use super::*;
138
    use crate::ast::{Expr, ExprKind, Placeholder, Variable, Whitespace};
139

            
140
    #[test]
141
    fn test_templatize_empty_string() {
142
        let encoded_string = EncodedString {
143
            source_info: SourceInfo::new(Pos::new(1, 1), Pos::new(1, 1)),
144
            chars: vec![],
145
        };
146
        assert_eq!(templatize(encoded_string).unwrap(), vec![]);
147
    }
148

            
149
    #[test]
150
    fn test_templatize_hello_world() {
151
        // Hi\u0020{{name}}!
152
        let encoded_string = EncodedString {
153
            source_info: SourceInfo::new(Pos::new(1, 1), Pos::new(1, 18)),
154
            chars: vec![
155
                ('H', "H".to_string(), Pos { line: 1, column: 1 }),
156
                ('i', "i".to_string(), Pos { line: 1, column: 2 }),
157
                (' ', "\\u0020".to_string(), Pos { line: 1, column: 3 }),
158
                ('{', "{".to_string(), Pos { line: 1, column: 9 }),
159
                (
160
                    '{',
161
                    "{".to_string(),
162
                    Pos {
163
                        line: 1,
164
                        column: 10,
165
                    },
166
                ),
167
                (
168
                    'n',
169
                    "n".to_string(),
170
                    Pos {
171
                        line: 1,
172
                        column: 11,
173
                    },
174
                ),
175
                (
176
                    'a',
177
                    "a".to_string(),
178
                    Pos {
179
                        line: 1,
180
                        column: 12,
181
                    },
182
                ),
183
                (
184
                    'm',
185
                    "m".to_string(),
186
                    Pos {
187
                        line: 1,
188
                        column: 13,
189
                    },
190
                ),
191
                (
192
                    'e',
193
                    "e".to_string(),
194
                    Pos {
195
                        line: 1,
196
                        column: 14,
197
                    },
198
                ),
199
                (
200
                    '}',
201
                    "}".to_string(),
202
                    Pos {
203
                        line: 1,
204
                        column: 15,
205
                    },
206
                ),
207
                (
208
                    '}',
209
                    "}".to_string(),
210
                    Pos {
211
                        line: 1,
212
                        column: 16,
213
                    },
214
                ),
215
                (
216
                    '!',
217
                    "!".to_string(),
218
                    Pos {
219
                        line: 1,
220
                        column: 17,
221
                    },
222
                ),
223
            ],
224
        };
225
        assert_eq!(
226
            templatize(encoded_string).unwrap(),
227
            vec![
228
                TemplateElement::String {
229
                    value: "Hi ".to_string(),
230
                    encoded: "Hi\\u0020".to_string(),
231
                },
232
                TemplateElement::Placeholder(Placeholder {
233
                    space0: Whitespace {
234
                        value: String::new(),
235
                        source_info: SourceInfo::new(Pos::new(1, 11), Pos::new(1, 11)),
236
                    },
237
                    expr: Expr {
238
                        kind: ExprKind::Variable(Variable {
239
                            name: "name".to_string(),
240
                            source_info: SourceInfo::new(Pos::new(1, 11), Pos::new(1, 15)),
241
                        }),
242
                        source_info: SourceInfo::new(Pos::new(1, 11), Pos::new(1, 15)),
243
                    },
244
                    space1: Whitespace {
245
                        value: String::new(),
246
                        source_info: SourceInfo::new(Pos::new(1, 15), Pos::new(1, 15)),
247
                    },
248
                }),
249
                TemplateElement::String {
250
                    value: "!".to_string(),
251
                    encoded: "!".to_string(),
252
                },
253
            ]
254
        );
255
    }
256

            
257
    #[test]
258
    fn test_templatize_expression_only() {
259
        // {{x}}!
260
        let encoded_string = EncodedString {
261
            source_info: SourceInfo::new(Pos::new(1, 1), Pos::new(1, 7)),
262
            chars: vec![
263
                ('{', "{".to_string(), Pos { line: 1, column: 1 }),
264
                ('{', "{".to_string(), Pos { line: 1, column: 2 }),
265
                ('x', "x".to_string(), Pos { line: 1, column: 3 }),
266
                ('}', "}".to_string(), Pos { line: 1, column: 4 }),
267
                ('}', "}".to_string(), Pos { line: 1, column: 4 }),
268
            ],
269
        };
270
        assert_eq!(
271
            templatize(encoded_string).unwrap(),
272
            vec![TemplateElement::Placeholder(Placeholder {
273
                space0: Whitespace {
274
                    value: String::new(),
275
                    source_info: SourceInfo::new(Pos::new(1, 3), Pos::new(1, 3)),
276
                },
277
                expr: Expr {
278
                    kind: ExprKind::Variable(Variable {
279
                        name: "x".to_string(),
280
                        source_info: SourceInfo::new(Pos::new(1, 3), Pos::new(1, 4)),
281
                    }),
282
                    source_info: SourceInfo::new(Pos::new(1, 3), Pos::new(1, 4))
283
                },
284
                space1: Whitespace {
285
                    value: String::new(),
286
                    source_info: SourceInfo::new(Pos::new(1, 4), Pos::new(1, 4)),
287
                },
288
            }),]
289
        );
290
    }
291

            
292
    #[test]
293
    fn test_templatize_error() {
294
        // missing closing
295
        // {{x
296
        let encoded_string = EncodedString {
297
            source_info: SourceInfo::new(Pos::new(1, 1), Pos::new(1, 4)),
298
            chars: vec![
299
                ('{', "{".to_string(), Pos { line: 1, column: 1 }),
300
                ('{', "{".to_string(), Pos { line: 1, column: 2 }),
301
                ('x', "x".to_string(), Pos { line: 1, column: 3 }),
302
            ],
303
        };
304
        let error = templatize(encoded_string).err().unwrap();
305
        assert_eq!(error.pos, Pos { line: 1, column: 4 });
306
        assert_eq!(
307
            error.kind,
308
            error::ParseErrorKind::Expecting {
309
                value: "}}".to_string()
310
            }
311
        );
312
        assert!(!error.recoverable);
313
    }
314

            
315
    #[test]
316
    fn test_escape_bracket() {
317
        // \{\{
318
        // This is a valid string "{{"
319
        let encoded_string = EncodedString {
320
            source_info: SourceInfo::new(Pos::new(1, 1), Pos::new(1, 4)),
321
            chars: vec![
322
                ('{', "\\{".to_string(), Pos { line: 1, column: 1 }),
323
                ('{', "\\{".to_string(), Pos { line: 1, column: 2 }),
324
            ],
325
        };
326
        assert_eq!(
327
            templatize(encoded_string).unwrap(),
328
            vec![TemplateElement::String {
329
                value: "{{".to_string(),
330
                encoded: "\\{\\{".to_string(),
331
            },]
332
        );
333
    }
334
}