1
/*
2
 * Hurl (https://hurl.dev)
3
 * Copyright (C) 2025 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
use crate::typing::SourceString;
23

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

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

            
37
47935
    let mut elements = vec![];
38
47935

            
39
47935
    let mut value = String::new();
40
47935
    let mut source = SourceString::new();
41
47935
    let mut state = State::String;
42
47935
    let mut expression_start = None;
43

            
44
1030880
    for (c, s, pos) in encoded_string.chars {
45
982950
        match state {
46
            State::String => {
47
971135
                if s.as_str() == "{" {
48
2730
                    state = State::FirstOpenBracket;
49
968405
                } else {
50
968405
                    value.push(c);
51
968405
                    source.push_str(&s);
52
                }
53
            }
54

            
55
            State::FirstOpenBracket => {
56
2710
                if s.as_str() == "{" {
57
1180
                    if !value.is_empty() {
58
310
                        elements.push(TemplateElement::String { value, source });
59
310
                        value = String::new();
60
310
                        source = SourceString::new();
61
                    }
62
1180
                    state = State::Template;
63
1530
                } else {
64
1530
                    value.push('{');
65
1530
                    source.push('{');
66
1530

            
67
1530
                    value.push(c);
68
1530
                    source.push_str(&s);
69
1530
                    state = State::String;
70
                }
71
            }
72

            
73
            State::Template => {
74
7930
                if expression_start.is_none() {
75
1180
                    expression_start = Some(pos);
76
                }
77
7930
                if s.as_str() == "}" {
78
1175
                    state = State::FirstCloseBracket;
79
6755
                } else {
80
6755
                    value.push(c);
81
6755
                    source.push_str(&s);
82
                }
83
            }
84

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

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

            
129
47925
    if !value.is_empty() {
130
46730
        elements.push(TemplateElement::String { value, source });
131
    }
132
47925
    Ok(elements)
133
}
134

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

            
138
    use super::*;
139
    use crate::ast::{Expr, ExprKind, Placeholder, Variable, Whitespace};
140
    use crate::typing::ToSource;
141

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

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

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

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

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