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 std::fmt::Display;
19

            
20
use crate::ast::*;
21
use crate::typing::Count;
22

            
23
/// Returns an HTML string of the Hurl file `hurl_file`.
24
///
25
/// If `standalone` is true, a complete HTML body with inline styling is returned.
26
/// Otherwise, a `<pre>` HTML tag is returned, without styling.
27
570
pub fn format(hurl_file: &HurlFile, standalone: bool) -> String {
28
570
    let mut fmt = HtmlFormatter::new();
29
570
    let body = fmt.fmt_hurl_file(hurl_file);
30
570
    if standalone {
31
5
        let css = include_str!("hurl.css");
32
5
        format!(
33
5
            r#"<!DOCTYPE html>
34
5
<html>
35
5
    <head>
36
5
        <meta charset="utf-8">
37
5
        <title>Hurl File</title>
38
5
        <style>
39
5
{css}
40
5
        </style>
41
5
    </head>
42
5
    <body>
43
5
{body}
44
5
    </body>
45
5
</html>
46
5
"#
47
5
        )
48
    } else {
49
565
        body.to_string()
50
    }
51
}
52

            
53
485
pub fn hurl_css() -> String {
54
485
    include_str!("hurl.css").to_string()
55
}
56

            
57
/// A HTML formatter for Hurl content.
58
struct HtmlFormatter {
59
    buffer: String,
60
}
61

            
62
impl HtmlFormatter {
63
570
    pub fn new() -> Self {
64
570
        HtmlFormatter {
65
570
            buffer: String::new(),
66
        }
67
    }
68

            
69
570
    pub fn fmt_hurl_file(&mut self, hurl_file: &HurlFile) -> &str {
70
570
        self.buffer.clear();
71
570
        self.fmt_pre_open("language-hurl");
72
1604
        hurl_file.entries.iter().for_each(|e| self.fmt_entry(e));
73
570
        self.fmt_lts(&hurl_file.line_terminators);
74
570
        self.fmt_pre_close();
75
570
        &self.buffer
76
    }
77

            
78
570
    fn fmt_pre_open(&mut self, class: &str) {
79
570
        self.buffer.push_str("<pre><code class=\"");
80
570
        self.buffer.push_str(class);
81
570
        self.buffer.push_str("\">");
82
    }
83

            
84
570
    fn fmt_pre_close(&mut self) {
85
570
        self.buffer.push_str("</code></pre>");
86
    }
87

            
88
18975
    fn fmt_span_open(&mut self, class: &str) {
89
18975
        self.buffer.push_str("<span class=\"");
90
18975
        self.buffer.push_str(class);
91
18975
        self.buffer.push_str("\">");
92
    }
93

            
94
19020
    fn fmt_span_close(&mut self) {
95
19020
        self.buffer.push_str("</span>");
96
    }
97

            
98
20800
    fn fmt_span(&mut self, class: &str, value: &str) {
99
20800
        self.buffer.push_str("<span class=\"");
100
20800
        self.buffer.push_str(class);
101
20800
        self.buffer.push_str("\">");
102
20800
        self.buffer.push_str(value);
103
20800
        self.buffer.push_str("</span>");
104
    }
105

            
106
1490
    fn fmt_entry(&mut self, entry: &Entry) {
107
1490
        self.fmt_span_open("hurl-entry");
108
1490
        self.fmt_request(&entry.request);
109
1490
        if let Some(response) = &entry.response {
110
1300
            self.fmt_response(response);
111
        }
112
1490
        self.fmt_span_close();
113
    }
114

            
115
1490
    fn fmt_request(&mut self, request: &Request) {
116
1490
        self.fmt_span_open("request");
117
1490
        self.fmt_lts(&request.line_terminators);
118
1490
        self.fmt_span_open("line");
119
1490
        self.fmt_space(&request.space0);
120
1490
        self.fmt_method(&request.method);
121
1490
        self.fmt_space(&request.space1);
122
1490
        let url = escape_xml(&request.url.to_encoded_string());
123
1490
        self.fmt_span("url", &url);
124
1490
        self.fmt_span_close();
125
1490
        self.fmt_lt(&request.line_terminator0);
126
1544
        request.headers.iter().for_each(|h| self.fmt_kv(h));
127
1577
        request.sections.iter().for_each(|s| self.fmt_section(s));
128
1490
        if let Some(body) = &request.body {
129
200
            self.fmt_body(body);
130
        }
131
1490
        self.fmt_span_close();
132
    }
133

            
134
1300
    fn fmt_response(&mut self, response: &Response) {
135
1300
        self.fmt_span_open("response");
136
1300
        self.fmt_lts(&response.line_terminators);
137
1300
        self.fmt_span_open("line");
138
1300
        self.fmt_space(&response.space0);
139
1300
        self.fmt_version(&response.version);
140
1300
        self.fmt_space(&response.space1);
141
1300
        self.fmt_status(&response.status);
142
1300
        self.fmt_span_close();
143
1300
        self.fmt_lt(&response.line_terminator0);
144
1352
        response.headers.iter().for_each(|h| self.fmt_kv(h));
145
1435
        response.sections.iter().for_each(|s| self.fmt_section(s));
146
1300
        if let Some(body) = &response.body {
147
390
            self.fmt_body(body);
148
        }
149
1300
        self.fmt_span_close();
150
    }
151

            
152
1490
    fn fmt_method(&mut self, method: &Method) {
153
1490
        self.fmt_span("method", &method.to_string());
154
    }
155

            
156
1300
    fn fmt_version(&mut self, version: &Version) {
157
1300
        self.fmt_span("version", &version.value.to_string());
158
    }
159

            
160
1300
    fn fmt_status(&mut self, status: &Status) {
161
1300
        self.fmt_number(status.value.to_string());
162
    }
163

            
164
1110
    fn fmt_section(&mut self, section: &Section) {
165
1110
        self.fmt_lts(&section.line_terminators);
166
1110
        self.fmt_space(&section.space0);
167
1110
        self.fmt_span_open("line");
168
1110
        let name = format!("[{}]", section.name());
169
1110
        self.fmt_span("section-header", &name);
170
1110
        self.fmt_span_close();
171
1110
        self.fmt_lt(&section.line_terminator0);
172
1110
        self.fmt_section_value(&section.value);
173
    }
174

            
175
1110
    fn fmt_section_value(&mut self, section_value: &SectionValue) {
176
1110
        match section_value {
177
2848
            SectionValue::Asserts(items) => items.iter().for_each(|item| self.fmt_assert(item)),
178
163
            SectionValue::QueryParams(items, _) => items.iter().for_each(|item| self.fmt_kv(item)),
179
15
            SectionValue::BasicAuth(item) => {
180
15
                if let Some(kv) = item {
181
15
                    self.fmt_kv(kv);
182
                }
183
            }
184
84
            SectionValue::FormParams(items, _) => items.iter().for_each(|item| self.fmt_kv(item)),
185
25
            SectionValue::MultipartFormData(items, _) => {
186
70
                items.iter().for_each(|item| self.fmt_multipart_param(item));
187
            }
188
12
            SectionValue::Cookies(items) => items.iter().for_each(|item| self.fmt_cookie(item)),
189
242
            SectionValue::Captures(items) => items.iter().for_each(|item| self.fmt_capture(item)),
190
300
            SectionValue::Options(items) => {
191
820
                items.iter().for_each(|item| self.fmt_entry_option(item));
192
            }
193
        }
194
    }
195

            
196
795
    fn fmt_kv(&mut self, kv: &KeyValue) {
197
795
        self.fmt_lts(&kv.line_terminators);
198
795
        self.fmt_span_open("line");
199
795
        self.fmt_space(&kv.space0);
200
795
        self.fmt_template(&kv.key);
201
795
        self.fmt_space(&kv.space1);
202
795
        self.buffer.push(':');
203
795
        self.fmt_space(&kv.space2);
204
795
        self.fmt_template(&kv.value);
205
795
        self.fmt_span_close();
206
795
        self.fmt_lt(&kv.line_terminator0);
207
    }
208

            
209
760
    fn fmt_entry_option(&mut self, option: &EntryOption) {
210
760
        self.fmt_lts(&option.line_terminators);
211
760
        self.fmt_span_open("line");
212
760
        self.fmt_space(&option.space0);
213
760
        self.fmt_string(option.kind.name());
214
760
        self.fmt_space(&option.space1);
215
760
        self.buffer.push(':');
216
760
        self.fmt_space(&option.space2);
217
760
        match &option.kind {
218
10
            OptionKind::AwsSigV4(value) => self.fmt_template(value),
219
10
            OptionKind::CaCertificate(filename) => self.fmt_filename(filename),
220
15
            OptionKind::ClientCert(filename) => self.fmt_filename(filename),
221
10
            OptionKind::ClientKey(filename) => self.fmt_filename(filename),
222
105
            OptionKind::Compressed(value) => self.fmt_bool_option(value),
223
10
            OptionKind::ConnectTo(value) => self.fmt_template(value),
224
20
            OptionKind::Delay(value) => self.fmt_duration_option(value),
225
90
            OptionKind::FollowLocation(value) => self.fmt_bool_option(value),
226
15
            OptionKind::FollowLocationTrusted(value) => self.fmt_bool_option(value),
227
35
            OptionKind::Http10(value) => self.fmt_bool_option(value),
228
25
            OptionKind::Http11(value) => self.fmt_bool_option(value),
229
10
            OptionKind::Http2(value) => self.fmt_bool_option(value),
230
10
            OptionKind::Http3(value) => self.fmt_bool_option(value),
231
20
            OptionKind::Insecure(value) => self.fmt_bool_option(value),
232
10
            OptionKind::IpV4(value) => self.fmt_bool_option(value),
233
10
            OptionKind::IpV6(value) => self.fmt_bool_option(value),
234
20
            OptionKind::MaxRedirect(value) => self.fmt_count_option(value),
235
10
            OptionKind::NetRc(value) => self.fmt_bool_option(value),
236
10
            OptionKind::NetRcFile(filename) => self.fmt_filename(filename),
237
10
            OptionKind::NetRcOptional(value) => self.fmt_bool_option(value),
238
10
            OptionKind::Output(filename) => self.fmt_filename(filename),
239
10
            OptionKind::PathAsIs(value) => self.fmt_bool_option(value),
240
20
            OptionKind::Proxy(value) => self.fmt_template(value),
241
15
            OptionKind::Repeat(value) => self.fmt_count_option(value),
242
10
            OptionKind::Resolve(value) => self.fmt_template(value),
243
35
            OptionKind::Retry(value) => self.fmt_count_option(value),
244
30
            OptionKind::RetryInterval(value) => self.fmt_duration_option(value),
245
10
            OptionKind::Skip(value) => self.fmt_bool_option(value),
246
10
            OptionKind::UnixSocket(value) => self.fmt_template(value),
247
20
            OptionKind::User(value) => self.fmt_template(value),
248
95
            OptionKind::Variable(value) => self.fmt_variable_definition(value),
249
25
            OptionKind::Verbose(value) => self.fmt_bool_option(value),
250
15
            OptionKind::VeryVerbose(value) => self.fmt_bool_option(value),
251
        };
252
760
        self.fmt_span_close();
253
760
        self.fmt_lt(&option.line_terminator0);
254
    }
255

            
256
70
    fn fmt_count_option(&mut self, count_option: &CountOption) {
257
70
        match count_option {
258
50
            CountOption::Literal(repeat) => self.fmt_count(*repeat),
259
20
            CountOption::Expression(expr) => self.fmt_expr(expr),
260
        }
261
    }
262

            
263
50
    fn fmt_count(&mut self, count: Count) {
264
50
        match count {
265
40
            Count::Finite(n) => self.fmt_number(n),
266
10
            Count::Infinite => self.fmt_number(-1),
267
        };
268
    }
269

            
270
95
    fn fmt_variable_definition(&mut self, option: &VariableDefinition) {
271
95
        self.buffer.push_str(option.name.as_str());
272
95
        self.fmt_space(&option.space1);
273
95
        self.buffer.push('=');
274
95
        self.fmt_variable_value(&option.value);
275
    }
276

            
277
95
    fn fmt_variable_value(&mut self, option: &VariableValue) {
278
95
        match option {
279
5
            VariableValue::Null => self.fmt_span("null", "null"),
280
5
            VariableValue::Bool(v) => self.fmt_bool(*v),
281
40
            VariableValue::Number(v) => self.fmt_number(v),
282
45
            VariableValue::String(t) => self.fmt_template(t),
283
        }
284
    }
285

            
286
65
    fn fmt_multipart_param(&mut self, param: &MultipartParam) {
287
65
        match param {
288
20
            MultipartParam::Param(param) => self.fmt_kv(param),
289
45
            MultipartParam::FileParam(param) => self.fmt_file_param(param),
290
        };
291
    }
292

            
293
45
    fn fmt_file_param(&mut self, param: &FileParam) {
294
45
        self.fmt_lts(&param.line_terminators);
295
45
        self.fmt_span_open("line");
296
45
        self.fmt_space(&param.space0);
297
45
        self.fmt_template(&param.key);
298
45
        self.fmt_space(&param.space1);
299
45
        self.buffer.push(':');
300
45
        self.fmt_space(&param.space2);
301
45
        self.fmt_file_value(&param.value);
302
45
        self.fmt_span_close();
303
45
        self.fmt_lt(&param.line_terminator0);
304
    }
305

            
306
45
    fn fmt_file_value(&mut self, file_value: &FileValue) {
307
45
        self.buffer.push_str("file,");
308
45
        self.fmt_space(&file_value.space0);
309
45
        self.fmt_filename(&file_value.filename);
310
45
        self.fmt_space(&file_value.space1);
311
45
        self.buffer.push(';');
312
45
        self.fmt_space(&file_value.space2);
313
45
        if let Some(content_type) = &file_value.content_type {
314
15
            self.fmt_string(content_type);
315
        }
316
    }
317

            
318
185
    fn fmt_filename(&mut self, filename: &Template) {
319
185
        self.fmt_span_open("filename");
320
185
        let s = filename.to_string().replace(' ', "\\ ");
321
185
        self.buffer.push_str(s.as_str());
322
185
        self.fmt_span_close();
323
    }
324

            
325
10
    fn fmt_cookie(&mut self, cookie: &Cookie) {
326
10
        self.fmt_lts(&cookie.line_terminators);
327
10
        self.fmt_span_open("line");
328
10
        self.fmt_space(&cookie.space0);
329
10
        self.fmt_template(&cookie.name);
330
10
        self.fmt_space(&cookie.space1);
331
10
        self.buffer.push(':');
332
10
        self.fmt_space(&cookie.space2);
333
10
        self.fmt_template(&cookie.value);
334
10
        self.fmt_span_close();
335
10
        self.fmt_lt(&cookie.line_terminator0);
336
    }
337

            
338
225
    fn fmt_capture(&mut self, capture: &Capture) {
339
225
        self.fmt_lts(&capture.line_terminators);
340
225
        self.fmt_span_open("line");
341
225
        self.fmt_space(&capture.space0);
342
225
        self.fmt_template(&capture.name);
343
225
        self.fmt_space(&capture.space1);
344
225
        self.buffer.push(':');
345
225
        self.fmt_space(&capture.space2);
346
225
        self.fmt_query(&capture.query);
347
225
        for (space, filter) in capture.filters.iter() {
348
40
            self.fmt_space(space);
349
40
            self.fmt_filter(filter);
350
        }
351
225
        self.fmt_span_close();
352
225
        self.fmt_lt(&capture.line_terminator0);
353
    }
354

            
355
2955
    fn fmt_query(&mut self, query: &Query) {
356
2955
        self.fmt_query_value(&query.value);
357
    }
358

            
359
2955
    fn fmt_query_value(&mut self, query_value: &QueryValue) {
360
2955
        match query_value {
361
25
            QueryValue::Status => self.fmt_span("query-type", "status"),
362
35
            QueryValue::Url => self.fmt_span("query-type", "url"),
363
285
            QueryValue::Header { space0, name } => {
364
285
                self.fmt_span("query-type", "header");
365
285
                self.fmt_space(space0);
366
285
                self.fmt_template(name);
367
            }
368
75
            QueryValue::Cookie { space0, expr } => {
369
75
                self.fmt_span("query-type", "cookie");
370
75
                self.fmt_space(space0);
371
75
                self.fmt_cookie_path(expr);
372
            }
373
245
            QueryValue::Body => self.fmt_span("query-type", "body"),
374
145
            QueryValue::Xpath { space0, expr } => {
375
145
                self.fmt_span("query-type", "xpath");
376
145
                self.fmt_space(space0);
377
145
                self.fmt_template(expr);
378
            }
379
1540
            QueryValue::Jsonpath { space0, expr } => {
380
1540
                self.fmt_span("query-type", "jsonpath");
381
1540
                self.fmt_space(space0);
382
1540
                self.fmt_template(expr);
383
            }
384
30
            QueryValue::Regex { space0, value } => {
385
30
                self.fmt_span("query-type", "regex");
386
30
                self.fmt_space(space0);
387
30
                self.fmt_regex_value(value);
388
            }
389
220
            QueryValue::Variable { space0, name } => {
390
220
                self.fmt_span("query-type", "variable");
391
220
                self.fmt_space(space0);
392
220
                self.fmt_template(name);
393
            }
394
15
            QueryValue::Duration => self.fmt_span("query-type", "duration"),
395
205
            QueryValue::Bytes => self.fmt_span("query-type", "bytes"),
396
45
            QueryValue::Sha256 => self.fmt_span("query-type", "sha256"),
397
40
            QueryValue::Md5 => self.fmt_span("query-type", "md5"),
398
            QueryValue::Certificate {
399
50
                space0,
400
50
                attribute_name: field,
401
50
            } => {
402
50
                self.fmt_span("query-type", "certificate");
403
50
                self.fmt_space(space0);
404
50
                self.fmt_certificate_attribute_name(field);
405
            }
406
        }
407
    }
408

            
409
105
    fn fmt_regex_value(&mut self, regex_value: &RegexValue) {
410
105
        match regex_value {
411
65
            RegexValue::Template(template) => self.fmt_template(template),
412
40
            RegexValue::Regex(regex) => self.fmt_regex(regex),
413
        }
414
    }
415

            
416
75
    fn fmt_cookie_path(&mut self, cookie_path: &CookiePath) {
417
75
        self.fmt_span_open("string");
418
75
        self.buffer.push('"');
419
75
        self.buffer
420
75
            .push_str(cookie_path.name.to_encoded_string().as_str());
421
75
        if let Some(attribute) = &cookie_path.attribute {
422
60
            self.buffer.push('[');
423
60
            self.fmt_cookie_attribute(attribute);
424
60
            self.buffer.push(']');
425
        }
426
75
        self.buffer.push('"');
427
75
        self.fmt_span_close();
428
    }
429

            
430
60
    fn fmt_cookie_attribute(&mut self, cookie_attribute: &CookieAttribute) {
431
60
        self.fmt_space(&cookie_attribute.space0);
432
60
        self.buffer.push_str(cookie_attribute.name.value().as_str());
433
60
        self.fmt_space(&cookie_attribute.space1);
434
    }
435

            
436
50
    fn fmt_certificate_attribute_name(&mut self, name: &CertificateAttributeName) {
437
50
        let value = match name {
438
5
            CertificateAttributeName::Subject => "Subject",
439
5
            CertificateAttributeName::Issuer => "Issuer",
440
15
            CertificateAttributeName::StartDate => "Start-Date",
441
20
            CertificateAttributeName::ExpireDate => "Expire-Date",
442
5
            CertificateAttributeName::SerialNumber => "Serial-Number",
443
        };
444
50
        self.fmt_span_open("string");
445
50
        self.buffer.push('"');
446
50
        self.buffer.push_str(value);
447
50
        self.buffer.push('"');
448
50
        self.fmt_span_close();
449
    }
450

            
451
2730
    fn fmt_assert(&mut self, assert: &Assert) {
452
2730
        self.fmt_lts(&assert.line_terminators);
453
2730
        self.fmt_span_open("line");
454
2730
        self.fmt_space(&assert.space0);
455
2730
        self.fmt_query(&assert.query);
456
2730
        for (space, filter) in assert.filters.iter() {
457
730
            self.fmt_space(space);
458
730
            self.fmt_filter(filter);
459
        }
460
2730
        self.fmt_space(&assert.space1);
461
2730
        self.fmt_predicate(&assert.predicate);
462
2730
        self.fmt_span_close();
463
2730
        self.fmt_lt(&assert.line_terminator0);
464
    }
465

            
466
2730
    fn fmt_predicate(&mut self, predicate: &Predicate) {
467
2730
        if predicate.not {
468
190
            self.fmt_span("not", "not");
469
190
            self.fmt_space(&predicate.space0);
470
        }
471
2730
        self.fmt_predicate_func(&predicate.predicate_func);
472
    }
473

            
474
2730
    fn fmt_predicate_func(&mut self, predicate_func: &PredicateFunc) {
475
2730
        self.fmt_predicate_func_value(&predicate_func.value);
476
    }
477

            
478
2730
    fn fmt_predicate_func_value(&mut self, value: &PredicateFuncValue) {
479
2730
        self.fmt_span_open("predicate-type");
480
2730
        self.buffer.push_str(&encode_html(value.name()));
481
2730
        self.fmt_span_close();
482
2730

            
483
2730
        match value {
484
1700
            PredicateFuncValue::Equal { space0, value, .. } => {
485
1700
                self.fmt_space(space0);
486
1700
                self.fmt_predicate_value(value);
487
            }
488
55
            PredicateFuncValue::NotEqual { space0, value, .. } => {
489
55
                self.fmt_space(space0);
490
55
                self.fmt_predicate_value(value);
491
            }
492
70
            PredicateFuncValue::GreaterThan { space0, value, .. } => {
493
70
                self.fmt_space(space0);
494
70
                self.fmt_predicate_value(value);
495
            }
496
15
            PredicateFuncValue::GreaterThanOrEqual { space0, value, .. } => {
497
15
                self.fmt_space(space0);
498
15
                self.fmt_predicate_value(value);
499
            }
500
50
            PredicateFuncValue::LessThan { space0, value, .. } => {
501
50
                self.fmt_space(space0);
502
50
                self.fmt_predicate_value(value);
503
            }
504
25
            PredicateFuncValue::LessThanOrEqual { space0, value, .. } => {
505
25
                self.fmt_space(space0);
506
25
                self.fmt_predicate_value(value);
507
            }
508
120
            PredicateFuncValue::StartWith { space0, value } => {
509
120
                self.fmt_space(space0);
510
120
                self.fmt_predicate_value(value);
511
            }
512
35
            PredicateFuncValue::EndWith { space0, value } => {
513
35
                self.fmt_space(space0);
514
35
                self.fmt_predicate_value(value);
515
            }
516
65
            PredicateFuncValue::Contain { space0, value } => {
517
65
                self.fmt_space(space0);
518
65
                self.fmt_predicate_value(value);
519
            }
520
80
            PredicateFuncValue::Include { space0, value } => {
521
80
                self.fmt_space(space0);
522
80
                self.fmt_predicate_value(value);
523
            }
524
85
            PredicateFuncValue::Match { space0, value } => {
525
85
                self.fmt_space(space0);
526
85
                self.fmt_predicate_value(value);
527
            }
528
15
            PredicateFuncValue::IsInteger => {}
529
20
            PredicateFuncValue::IsFloat => {}
530
15
            PredicateFuncValue::IsBoolean => {}
531
15
            PredicateFuncValue::IsString => {}
532
40
            PredicateFuncValue::IsCollection => {}
533
20
            PredicateFuncValue::IsDate => {}
534
40
            PredicateFuncValue::IsIsoDate => {}
535
195
            PredicateFuncValue::Exist => {}
536
50
            PredicateFuncValue::IsEmpty => {}
537
20
            PredicateFuncValue::IsNumber => {}
538
        }
539
    }
540

            
541
2300
    fn fmt_predicate_value(&mut self, predicate_value: &PredicateValue) {
542
2300
        match predicate_value {
543
1050
            PredicateValue::String(value) => self.fmt_template(value),
544
45
            PredicateValue::MultilineString(value) => self.fmt_multiline_string(value, false),
545
730
            PredicateValue::Number(value) => self.fmt_number(value),
546
60
            PredicateValue::Bool(value) => self.fmt_bool(*value),
547
15
            PredicateValue::File(value) => self.fmt_file(value),
548
215
            PredicateValue::Hex(value) => self.fmt_hex(value),
549
10
            PredicateValue::Base64(value) => self.fmt_base64(value),
550
100
            PredicateValue::Expression(value) => self.fmt_expr(value),
551
25
            PredicateValue::Null => self.fmt_span("null", "null"),
552
50
            PredicateValue::Regex(value) => self.fmt_regex(value),
553
        };
554
    }
555

            
556
185
    fn fmt_multiline_string(&mut self, multiline_string: &MultilineString, as_body: bool) {
557
185
        // The multiline spans multiple newlines. We distinguish cases for multiline
558
185
        // as a body and multiline as a predicate value. When used as a body, we can embed
559
185
        // span lines with the multiline span. Used as a predicate, we have to break the multiline
560
185
        // span in two parts.
561
185
        //
562
185
        // # Case 1: multiline string as a body
563
185
        //
564
185
        // ~~~hurl
565
185
        // GET https://foo.com
566
185
        // ```
567
185
        // line1
568
185
        // line2
569
185
        // line3
570
185
        // ```
571
185
        // ~~~
572
185
        //
573
185
        // We embed span lines inside the span for the body:
574
185
        //
575
185
        // ```html
576
185
        // ...
577
185
        // <span class="multiline">
578
185
        //   <span class="line">```</span>
579
185
        //   <span class="line">line1</span>
580
185
        //   <span class="line">line2</span>
581
185
        //   <span class="line">line3</span>
582
185
        //   <span class="line">```</span>
583
185
        // </span>
584
185
        // ```
585
185
        //
586
185
        // # Case 1: multiline string as a predicate value
587
185
        //
588
185
        // ~~~hurl
589
185
        // GET https://foo.com
590
185
        // HTTP 200
591
185
        // [Asserts]
592
185
        // body == ```
593
185
        // line1
594
185
        // line2
595
185
        // line3
596
185
        // ```
597
185
        // ~~~
598
185
        //
599
185
        // ```html
600
185
        // ...
601
185
        // <span class="line">body ==
602
185
        //   <span class="multiline">```</span>
603
185
        // </span>
604
185
        // <span class="multiline">
605
185
        //   <span class="line">line1</span>
606
185
        //   <span class="line">line2</span>
607
185
        //   <span class="line">line3</span>
608
185
        //   <span class="line">```</span>
609
185
        // </span>
610
185
        // ```
611
185
        let lang = multiline_string.lang();
612
185
        if as_body {
613
140
            let mut attributes = String::new();
614
140
            for (i, attribute) in multiline_string.attributes.iter().enumerate() {
615
10
                if i > 0 || !lang.is_empty() {
616
5
                    attributes.push_str(", ");
617
                }
618
10
                attributes.push_str(attribute.to_string().as_str());
619
            }
620
            // Keep original encoded string
621
140
            let multiline_string_encoded = multiline_string.kind.to_encoded_string();
622
140
            let body = format!("```{lang}{attributes}\n{multiline_string_encoded}```");
623
140
            let body = format_multilines(&body);
624
140
            self.fmt_span("multiline", &body);
625
45
        } else {
626
45
            let head = format!("```{lang}");
627
45
            self.fmt_span("multiline", &head);
628
45
            // We close the current span line opened by the assert
629
45
            self.fmt_span_close();
630
45
            self.buffer.push('\n');
631
45
            let tail = format!("{multiline_string}```");
632
45
            let tail = format_multilines(&tail);
633
45
            self.fmt_span("multiline", &tail);
634
45
            // As we have added a span close, we must remove one to have the right number
635
45
            // of span. The current span line will add a closing span.
636
45
            pop_str(&mut self.buffer, "</span>");
637
        }
638
    }
639

            
640
590
    fn fmt_body(&mut self, body: &Body) {
641
590
        self.fmt_lts(&body.line_terminators);
642
590
        self.fmt_space(&body.space0);
643
590
        self.fmt_bytes(&body.value);
644
590
        self.fmt_lt(&body.line_terminator0);
645
    }
646

            
647
590
    fn fmt_bytes(&mut self, bytes: &Bytes) {
648
590
        match bytes {
649
55
            Bytes::Base64(value) => {
650
55
                self.fmt_span_open("line");
651
55
                self.fmt_base64(value);
652
55
                self.fmt_span_close();
653
            }
654
70
            Bytes::File(value) => {
655
70
                self.fmt_span_open("line");
656
70
                self.fmt_file(value);
657
70
                self.fmt_span_close();
658
            }
659
30
            Bytes::Hex(value) => {
660
30
                self.fmt_span_open("line");
661
30
                self.fmt_hex(value);
662
30
                self.fmt_span_close();
663
            }
664
220
            Bytes::OnelineString(value) => {
665
220
                self.fmt_span_open("line");
666
220
                self.fmt_template(value);
667
220
                self.fmt_span_close();
668
            }
669
50
            Bytes::Json(value) => self.fmt_json_value(value),
670
140
            Bytes::MultilineString(value) => self.fmt_multiline_string(value, true),
671
25
            Bytes::Xml(value) => self.fmt_xml(value),
672
        }
673
    }
674

            
675
6505
    fn fmt_string(&mut self, value: &str) {
676
6505
        self.fmt_span("string", value);
677
    }
678

            
679
65
    fn fmt_bool(&mut self, value: bool) {
680
65
        self.fmt_span("boolean", &value.to_string());
681
    }
682

            
683
410
    fn fmt_bool_option(&mut self, value: &BooleanOption) {
684
410
        match value {
685
325
            BooleanOption::Literal(value) => self.fmt_span("boolean", &value.to_string()),
686
85
            BooleanOption::Expression(value) => self.fmt_expr(value),
687
        }
688
    }
689

            
690
50
    fn fmt_duration_option(&mut self, value: &DurationOption) {
691
50
        match value {
692
40
            DurationOption::Literal(value) => {
693
40
                self.fmt_span("number", &value.value.to_string());
694
40
                if let Some(unit) = value.unit {
695
30
                    self.fmt_span("unit", &unit.to_string());
696
                }
697
            }
698
10
            DurationOption::Expression(value) => self.fmt_expr(value),
699
        }
700
    }
701

            
702
2235
    fn fmt_number<T: Sized + Display>(&mut self, value: T) {
703
2235
        self.fmt_span("number", &value.to_string());
704
    }
705

            
706
25
    fn fmt_xml(&mut self, value: &str) {
707
25
        let xml = format_multilines(value);
708
25
        self.fmt_span("xml", &xml);
709
    }
710

            
711
50
    fn fmt_json_value(&mut self, json_value: &JsonValue) {
712
50
        let json = format_multilines(&json_value.encoded());
713
50
        self.fmt_span("json", &json);
714
    }
715

            
716
37250
    fn fmt_space(&mut self, space: &Whitespace) {
717
37250
        let Whitespace { value, .. } = space;
718
37250
        if !value.is_empty() {
719
13750
            self.buffer.push_str(value);
720
23500
        };
721
    }
722

            
723
11870
    fn fmt_lt(&mut self, lt: &LineTerminator) {
724
11870
        self.fmt_space(&lt.space0);
725
11870
        if let Some(v) = &lt.comment {
726
1345
            self.fmt_comment(v);
727
        }
728
11870
        self.buffer.push_str(lt.newline.value.as_str());
729
    }
730

            
731
1345
    fn fmt_comment(&mut self, comment: &Comment) {
732
1345
        let comment = format!("#{}", escape_xml(&comment.value));
733
1345
        self.fmt_span("comment", &comment);
734
    }
735

            
736
85
    fn fmt_file(&mut self, file: &File) {
737
85
        self.buffer.push_str("file,");
738
85
        self.fmt_space(&file.space0);
739
85
        self.fmt_filename(&file.filename);
740
85
        self.fmt_space(&file.space1);
741
85
        self.buffer.push(';');
742
    }
743

            
744
65
    fn fmt_base64(&mut self, base64: &Base64) {
745
65
        self.buffer.push_str("base64,");
746
65
        self.fmt_space(&base64.space0);
747
65
        self.fmt_span("base64", &base64.encoded);
748
65
        self.fmt_space(&base64.space1);
749
65
        self.buffer.push(';');
750
    }
751

            
752
245
    fn fmt_hex(&mut self, hex: &Hex) {
753
245
        self.buffer.push_str("hex,");
754
245
        self.fmt_space(&hex.space0);
755
245
        self.fmt_span("hex", &hex.encoded);
756
245
        self.fmt_space(&hex.space1);
757
245
        self.buffer.push(';');
758
    }
759

            
760
90
    fn fmt_regex(&mut self, regex: &Regex) {
761
90
        let s = str::replace(regex.inner.as_str(), "/", "\\/");
762
90
        let regex = format!("/{s}/");
763
90
        self.fmt_span("regex", &regex);
764
    }
765

            
766
5730
    fn fmt_template(&mut self, template: &Template) {
767
5730
        let s = template.to_encoded_string();
768
5730
        self.fmt_string(&escape_xml(&s));
769
    }
770

            
771
215
    fn fmt_expr(&mut self, expr: &Expr) {
772
215
        let expr = format!("{{{{{}}}}}", &expr.to_string());
773
215
        self.fmt_span("expr", &expr);
774
    }
775

            
776
770
    fn fmt_filter(&mut self, filter: &Filter) {
777
770
        self.fmt_filter_value(&filter.value);
778
    }
779

            
780
770
    fn fmt_filter_value(&mut self, filter_value: &FilterValue) {
781
770
        match filter_value {
782
220
            FilterValue::Count => self.fmt_span("filter-type", "count"),
783
10
            FilterValue::DaysAfterNow => self.fmt_span("filter-type", "daysAfterNow"),
784
20
            FilterValue::DaysBeforeNow => self.fmt_span("filter-type", "daysBeforeNow"),
785
45
            FilterValue::Decode { space0, encoding } => {
786
45
                self.fmt_span("filter-type", "decode");
787
45
                self.fmt_space(space0);
788
45
                self.fmt_template(encoding);
789
            }
790
35
            FilterValue::Format { space0, fmt } => {
791
35
                self.fmt_span("filter-type", "format");
792
35
                self.fmt_space(space0);
793
35
                self.fmt_template(fmt);
794
            }
795
20
            FilterValue::HtmlEscape => self.fmt_span("filter-type", "htmlEscape"),
796
30
            FilterValue::HtmlUnescape => self.fmt_span("filter-type", "htmlUnescape"),
797
20
            FilterValue::JsonPath { space0, expr } => {
798
20
                self.fmt_span("filter-type", "jsonpath");
799
20
                self.fmt_space(space0);
800
20
                self.fmt_template(expr);
801
            }
802
115
            FilterValue::Nth { space0, n: value } => {
803
115
                self.fmt_span("filter-type", "nth");
804
115
                self.fmt_space(space0);
805
115
                self.fmt_number(value);
806
            }
807
40
            FilterValue::Regex { space0, value } => {
808
40
                self.fmt_span("filter-type", "regex");
809
40
                self.fmt_space(space0);
810
40
                self.fmt_regex_value(value);
811
            }
812
            FilterValue::Replace {
813
35
                space0,
814
35
                old_value,
815
35
                space1,
816
35
                new_value,
817
35
            } => {
818
35
                self.fmt_span("filter-type", "replace");
819
35
                self.fmt_space(space0);
820
35
                self.fmt_regex_value(old_value);
821
35
                self.fmt_space(space1);
822
35
                self.fmt_template(new_value);
823
            }
824
15
            FilterValue::Split { space0, sep } => {
825
15
                self.fmt_span("filter-type", "split");
826
15
                self.fmt_space(space0);
827
15
                self.fmt_template(sep);
828
            }
829
30
            FilterValue::ToDate { space0, fmt } => {
830
30
                self.fmt_span("filter-type", "toDate");
831
30
                self.fmt_space(space0);
832
30
                self.fmt_template(fmt);
833
            }
834
30
            FilterValue::ToFloat => self.fmt_span("filter-type", "toFloat"),
835
45
            FilterValue::ToInt => self.fmt_span("filter-type", "toInt"),
836
20
            FilterValue::UrlDecode => self.fmt_span("filter-type", "urlDecode"),
837
20
            FilterValue::UrlEncode => self.fmt_span("filter-type", "urlEncode"),
838
20
            FilterValue::XPath { space0, expr } => {
839
20
                self.fmt_span("filter-type", "xpath");
840
20
                self.fmt_space(space0);
841
20
                self.fmt_template(expr);
842
            }
843
        };
844
    }
845

            
846
9625
    fn fmt_lts(&mut self, line_terminators: &[LineTerminator]) {
847
12440
        for line_terminator in line_terminators {
848
2815
            self.fmt_span_open("line");
849
2815
            if line_terminator.newline.value.is_empty() {
850
5
                self.buffer.push_str("<br />");
851
            }
852
2815
            self.fmt_span_close();
853
2815
            self.fmt_lt(line_terminator);
854
        }
855
    }
856
}
857

            
858
13705
fn escape_xml(s: &str) -> String {
859
13705
    s.replace('&', "&amp;")
860
13705
        .replace('<', "&lt;")
861
13705
        .replace('>', "&gt;")
862
}
863

            
864
impl Template {
865
7435
    fn to_encoded_string(&self) -> String {
866
7435
        let mut s = String::new();
867
7435
        if let Some(d) = self.delimiter {
868
3725
            s.push(d);
869
        }
870
7435
        for element in self.elements.iter() {
871
7435
            let elem_str = match element {
872
7235
                TemplateElement::String { encoded, .. } => encoded.to_string(),
873
200
                TemplateElement::Expression(expr) => format!("{{{{{expr}}}}}"),
874
            };
875
7435
            s.push_str(elem_str.as_str());
876
        }
877
7435
        if let Some(d) = self.delimiter {
878
3725
            s.push(d);
879
        }
880
7435
        s
881
    }
882
}
883

            
884
impl MultilineStringKind {
885
140
    fn to_encoded_string(&self) -> String {
886
140
        match self {
887
55
            MultilineStringKind::Text(text)
888
25
            | MultilineStringKind::Json(text)
889
100
            | MultilineStringKind::Xml(text) => text.value.to_encoded_string(),
890
40
            MultilineStringKind::GraphQl(graphql) => graphql.to_encoded_string(),
891
        }
892
    }
893
}
894

            
895
impl GraphQl {
896
40
    fn to_encoded_string(&self) -> String {
897
40
        let mut s = self.value.to_encoded_string();
898
40
        if let Some(vars) = &self.variables {
899
10
            s.push_str(&vars.to_encoded_string());
900
        }
901
40
        s
902
    }
903
}
904

            
905
impl GraphQlVariables {
906
10
    fn to_encoded_string(&self) -> String {
907
10
        let mut s = "variables".to_string();
908
10
        s.push_str(&self.space.value);
909
10
        s.push_str(&self.value.encoded());
910
10
        s.push_str(&self.whitespace.value);
911
10
        s
912
    }
913
}
914

            
915
2730
fn encode_html(s: String) -> String {
916
2730
    s.replace('>', "&gt;").replace('<', "&lt;")
917
}
918

            
919
260
fn format_multilines(s: &str) -> String {
920
260
    regex::Regex::new(r"\n|\r\n")
921
260
        .unwrap()
922
260
        .split(s)
923
5192
        .map(|l| format!("<span class=\"line\">{}</span>", escape_xml(l)))
924
260
        .collect::<Vec<String>>()
925
260
        .join("\n")
926
}
927

            
928
45
fn pop_str(string: &mut String, suffix: &str) {
929
45
    let len = string.len();
930
45
    let n = suffix.len();
931
45
    let len = len - n;
932
45
    string.truncate(len);
933
}
934

            
935
#[cfg(test)]
936
mod tests {
937
    use super::*;
938
    use crate::reader::Pos;
939

            
940
    #[test]
941
    fn test_multiline_string() {
942
        // ```
943
        // line1
944
        // line2
945
        // ```
946
        let kind = MultilineStringKind::Text(Text {
947
            space: Whitespace {
948
                value: String::new(),
949
                source_info: SourceInfo {
950
                    start: Pos { line: 1, column: 4 },
951
                    end: Pos { line: 1, column: 4 },
952
                },
953
            },
954
            newline: Whitespace {
955
                value: "\n".to_string(),
956
                source_info: SourceInfo {
957
                    start: Pos { line: 1, column: 4 },
958
                    end: Pos { line: 2, column: 1 },
959
                },
960
            },
961
            value: Template {
962
                delimiter: None,
963
                elements: vec![TemplateElement::String {
964
                    value: "line1\nline2\n".to_string(),
965
                    encoded: "line1\nline2\n".to_string(),
966
                }],
967
                source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
968
            },
969
        });
970
        let attributes = vec![];
971
        let multiline_string = MultilineString { kind, attributes };
972
        let mut fmt = HtmlFormatter::new();
973
        fmt.fmt_multiline_string(&multiline_string, true);
974
        assert_eq!(
975
            fmt.buffer,
976
            "<span class=\"multiline\">\
977
                <span class=\"line\">```</span>\n\
978
                <span class=\"line\">line1</span>\n\
979
                <span class=\"line\">line2</span>\n\
980
                <span class=\"line\">```</span>\
981
            </span>"
982
        );
983

            
984
        let mut fmt = HtmlFormatter::new();
985
        fmt.fmt_multiline_string(&multiline_string, false);
986
        assert_eq!(
987
            fmt.buffer,
988
            "<span class=\"multiline\">```</span>\
989
        </span>\n\
990
        <span class=\"multiline\">\
991
            <span class=\"line\">line1</span>\n\
992
            <span class=\"line\">line2</span>\n\
993
            <span class=\"line\">```</span>"
994
        );
995
    }
996

            
997
    #[test]
998
    fn test_multilines() {
999
        assert_eq!(
            format_multilines("{\n   \"id\": 1\n}"),
            "<span class=\"line\">{</span>\n\
            <span class=\"line\">   \"id\": 1</span>\n\
            <span class=\"line\">}</span>"
        );
        assert_eq!(
            format_multilines(
                "<?xml version=\"1.0\"?>\n\
            <drink>café</drink>"
            ),
            "<span class=\"line\">&lt;?xml version=\"1.0\"?&gt;</span>\n\
            <span class=\"line\">&lt;drink&gt;café&lt;/drink&gt;</span>"
        );
        assert_eq!(
            format_multilines("Hello\n"),
            "<span class=\"line\">Hello</span>\n\
            <span class=\"line\"></span>"
        );
    }
    #[test]
    fn test_json() {
        let mut fmt = HtmlFormatter::new();
        let value = JsonValue::Object {
            space0: String::new(),
            elements: vec![JsonObjectElement {
                space0: "\n   ".to_string(),
                name: Template {
                    delimiter: Some('"'),
                    elements: vec![TemplateElement::String {
                        value: "id".to_string(),
                        encoded: "id".to_string(),
                    }],
                    source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
                },
                space1: String::new(),
                space2: " ".to_string(),
                value: JsonValue::Number("1".to_string()),
                space3: "\n".to_string(),
            }],
        };
        fmt.fmt_json_value(&value);
        assert_eq!(
            fmt.buffer,
            "<span class=\"json\">\
                <span class=\"line\">{</span>\n\
                <span class=\"line\">   \"id\": 1</span>\n\
                <span class=\"line\">}</span>\
            </span>"
        );
    }
    #[test]
    fn test_json_encoded_newline() {
        let mut fmt = HtmlFormatter::new();
        let value = JsonValue::String(Template {
            delimiter: Some('"'),
            elements: vec![TemplateElement::String {
                value: "\n".to_string(),
                encoded: "\\n".to_string(),
            }],
            source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
        });
        fmt.fmt_json_value(&value);
        assert_eq!(
            fmt.buffer,
            "<span class=\"json\"><span class=\"line\">\"\\n\"</span></span>"
        );
    }
    #[test]
    fn test_xml() {
        let mut fmt = HtmlFormatter::new();
        let value = "<?xml version=\"1.0\"?>\n<drink>café</drink>";
        fmt.fmt_xml(value);
        assert_eq!(
            fmt.buffer,
            "<span class=\"xml\"><span class=\"line\">&lt;?xml version=\"1.0\"?&gt;</span>\n<span class=\"line\">&lt;drink&gt;café&lt;/drink&gt;</span></span>"
        );
    }
    #[test]
    fn test_xml_escape() {
        assert_eq!(escape_xml("hello"), "hello");
        assert_eq!(
            escape_xml("<?xml version=\"1.0\"?>"),
            "&lt;?xml version=\"1.0\"?&gt;"
        );
    }
}