1
/*
2
 * Hurl (https://hurl.dev)
3
 * Copyright (C) 2026 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::reader::{Pos, Reader};
20
use crate::types::SourceString;
21

            
22
use super::primitives::zero_or_more_spaces;
23
use super::{ParseResult, error, expr};
24

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

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

            
38
52752
    let mut elements = vec![];
39

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

            
45
1069876
    for (c, s, pos) in encoded_string.chars {
46
1069876
        match state {
47
            State::String => {
48
1058180
                if s.as_str() == "{" {
49
2572
                    state = State::FirstOpenBracket;
50
1055608
                } else {
51
1055608
                    value.push(c);
52
1055608
                    source.push_str(&s);
53
                }
54
            }
55

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

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

            
74
            State::Template => {
75
7952
                if expression_start.is_none() {
76
1192
                    expression_start = Some(pos);
77
                }
78
7952
                if s.as_str() == "}" {
79
1188
                    state = State::FirstCloseBracket;
80
6764
                } else {
81
6764
                    value.push(c);
82
6764
                    source.push_str(&s);
83
                }
84
            }
85

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

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

            
130
52744
    if !value.is_empty() {
131
51520
        elements.push(TemplateElement::String { value, source });
132
    }
133
52744
    Ok(elements)
134
}
135

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

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

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

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

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

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

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