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
575
pub fn format(hurl_file: &HurlFile, standalone: bool) -> String {
28
575
    let mut fmt = HtmlFormatter::new();
29
575
    let body = fmt.fmt_hurl_file(hurl_file);
30
575
    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
570
        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
575
    pub fn new() -> Self {
64
575
        HtmlFormatter {
65
575
            buffer: String::new(),
66
        }
67
    }
68

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

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

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

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

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

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

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

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

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

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

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

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

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

            
175
1130
    fn fmt_section_value(&mut self, section_value: &SectionValue) {
176
1130
        match section_value {
177
2896
            SectionValue::Asserts(items) => items.iter().for_each(|item| self.fmt_assert(item)),
178
184
            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
845
                items.iter().for_each(|item| self.fmt_entry_option(item));
192
            }
193
        }
194
    }
195

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

            
209
785
    fn fmt_entry_option(&mut self, option: &EntryOption) {
210
785
        self.fmt_lts(&option.line_terminators);
211
785
        self.fmt_span_open("line");
212
785
        self.fmt_space(&option.space0);
213
785
        self.fmt_string(option.kind.name());
214
785
        self.fmt_space(&option.space1);
215
785
        self.buffer.push(':');
216
785
        self.fmt_space(&option.space2);
217
785
        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
10
            OptionKind::ConnectTimeout(value) => self.fmt_duration_option(value),
225
20
            OptionKind::Delay(value) => self.fmt_duration_option(value),
226
90
            OptionKind::FollowLocation(value) => self.fmt_bool_option(value),
227
15
            OptionKind::FollowLocationTrusted(value) => self.fmt_bool_option(value),
228
35
            OptionKind::Http10(value) => self.fmt_bool_option(value),
229
25
            OptionKind::Http11(value) => self.fmt_bool_option(value),
230
10
            OptionKind::Http2(value) => self.fmt_bool_option(value),
231
10
            OptionKind::Http3(value) => self.fmt_bool_option(value),
232
20
            OptionKind::Insecure(value) => self.fmt_bool_option(value),
233
10
            OptionKind::IpV4(value) => self.fmt_bool_option(value),
234
10
            OptionKind::IpV6(value) => self.fmt_bool_option(value),
235
10
            OptionKind::LimitRate(value) => self.fmt_natural_option(value),
236
20
            OptionKind::MaxRedirect(value) => self.fmt_count_option(value),
237
10
            OptionKind::NetRc(value) => self.fmt_bool_option(value),
238
10
            OptionKind::NetRcFile(filename) => self.fmt_filename(filename),
239
10
            OptionKind::NetRcOptional(value) => self.fmt_bool_option(value),
240
10
            OptionKind::Output(filename) => self.fmt_filename(filename),
241
10
            OptionKind::PathAsIs(value) => self.fmt_bool_option(value),
242
20
            OptionKind::Proxy(value) => self.fmt_template(value),
243
15
            OptionKind::Repeat(value) => self.fmt_count_option(value),
244
10
            OptionKind::Resolve(value) => self.fmt_template(value),
245
35
            OptionKind::Retry(value) => self.fmt_count_option(value),
246
30
            OptionKind::RetryInterval(value) => self.fmt_duration_option(value),
247
10
            OptionKind::Skip(value) => self.fmt_bool_option(value),
248
10
            OptionKind::UnixSocket(value) => self.fmt_template(value),
249
20
            OptionKind::User(value) => self.fmt_template(value),
250
100
            OptionKind::Variable(value) => self.fmt_variable_definition(value),
251
25
            OptionKind::Verbose(value) => self.fmt_bool_option(value),
252
15
            OptionKind::VeryVerbose(value) => self.fmt_bool_option(value),
253
        };
254
785
        self.fmt_span_close();
255
785
        self.fmt_lt(&option.line_terminator0);
256
    }
257

            
258
70
    fn fmt_count_option(&mut self, count_option: &CountOption) {
259
70
        match count_option {
260
50
            CountOption::Literal(repeat) => self.fmt_count(*repeat),
261
20
            CountOption::Placeholder(placeholder) => self.fmt_placeholder(placeholder),
262
        }
263
    }
264

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

            
272
100
    fn fmt_variable_definition(&mut self, option: &VariableDefinition) {
273
100
        self.buffer.push_str(option.name.as_str());
274
100
        self.fmt_space(&option.space0);
275
100
        self.buffer.push('=');
276
100
        self.fmt_space(&option.space1);
277
100
        self.fmt_variable_value(&option.value);
278
    }
279

            
280
100
    fn fmt_variable_value(&mut self, option: &VariableValue) {
281
100
        match option {
282
5
            VariableValue::Null => self.fmt_span("null", "null"),
283
5
            VariableValue::Bool(v) => self.fmt_bool(*v),
284
40
            VariableValue::Number(v) => self.fmt_number(v),
285
50
            VariableValue::String(t) => self.fmt_template(t),
286
        }
287
    }
288

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

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

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

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

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

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

            
358
3000
    fn fmt_query(&mut self, query: &Query) {
359
3000
        self.fmt_query_value(&query.value);
360
    }
361

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

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

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

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

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

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

            
469
2775
    fn fmt_predicate(&mut self, predicate: &Predicate) {
470
2775
        if predicate.not {
471
190
            self.fmt_span("not", "not");
472
190
            self.fmt_space(&predicate.space0);
473
        }
474
2775
        self.fmt_predicate_func(&predicate.predicate_func);
475
    }
476

            
477
2775
    fn fmt_predicate_func(&mut self, predicate_func: &PredicateFunc) {
478
2775
        self.fmt_predicate_func_value(&predicate_func.value);
479
    }
480

            
481
2775
    fn fmt_predicate_func_value(&mut self, value: &PredicateFuncValue) {
482
2775
        self.fmt_span_open("predicate-type");
483
2775
        self.buffer.push_str(&encode_html(value.name()));
484
2775
        self.fmt_span_close();
485
2775

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

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

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

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

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

            
678
6650
    fn fmt_string(&mut self, value: &str) {
679
6650
        self.fmt_span("string", value);
680
    }
681

            
682
65
    fn fmt_bool(&mut self, value: bool) {
683
65
        self.fmt_span("boolean", &value.to_string());
684
    }
685

            
686
410
    fn fmt_bool_option(&mut self, value: &BooleanOption) {
687
410
        match value {
688
325
            BooleanOption::Literal(value) => self.fmt_span("boolean", &value.to_string()),
689
85
            BooleanOption::Placeholder(value) => self.fmt_placeholder(value),
690
        }
691
    }
692

            
693
10
    fn fmt_natural_option(&mut self, value: &NaturalOption) {
694
10
        match value {
695
5
            NaturalOption::Literal(value) => self.fmt_span("number", &value.to_string()),
696
5
            NaturalOption::Placeholder(value) => self.fmt_placeholder(value),
697
        }
698
    }
699

            
700
60
    fn fmt_duration_option(&mut self, value: &DurationOption) {
701
60
        match value {
702
45
            DurationOption::Literal(value) => {
703
45
                self.fmt_span("number", &value.value.to_string());
704
45
                if let Some(unit) = value.unit {
705
35
                    self.fmt_span("unit", &unit.to_string());
706
                }
707
            }
708
15
            DurationOption::Placeholder(value) => self.fmt_placeholder(value),
709
        }
710
    }
711

            
712
2260
    fn fmt_number<T: Sized + Display>(&mut self, value: T) {
713
2260
        self.fmt_span("number", &value.to_string());
714
    }
715

            
716
25
    fn fmt_xml(&mut self, value: &str) {
717
25
        let xml = format_multilines(value);
718
25
        self.fmt_span("xml", &xml);
719
    }
720

            
721
50
    fn fmt_json_value(&mut self, json_value: &JsonValue) {
722
50
        let json = format_multilines(&json_value.encoded());
723
50
        self.fmt_span("json", &json);
724
    }
725

            
726
38035
    fn fmt_space(&mut self, space: &Whitespace) {
727
38035
        let Whitespace { value, .. } = space;
728
38035
        if !value.is_empty() {
729
13980
            self.buffer.push_str(value);
730
24055
        };
731
    }
732

            
733
12110
    fn fmt_lt(&mut self, lt: &LineTerminator) {
734
12110
        self.fmt_space(&lt.space0);
735
12110
        if let Some(v) = &lt.comment {
736
1360
            self.fmt_comment(v);
737
        }
738
12110
        self.buffer.push_str(lt.newline.value.as_str());
739
    }
740

            
741
1360
    fn fmt_comment(&mut self, comment: &Comment) {
742
1360
        let comment = format!("#{}", escape_xml(&comment.value));
743
1360
        self.fmt_span("comment", &comment);
744
    }
745

            
746
85
    fn fmt_file(&mut self, file: &File) {
747
85
        self.buffer.push_str("file,");
748
85
        self.fmt_space(&file.space0);
749
85
        self.fmt_filename(&file.filename);
750
85
        self.fmt_space(&file.space1);
751
85
        self.buffer.push(';');
752
    }
753

            
754
65
    fn fmt_base64(&mut self, base64: &Base64) {
755
65
        self.buffer.push_str("base64,");
756
65
        self.fmt_space(&base64.space0);
757
65
        self.fmt_span("base64", &base64.encoded);
758
65
        self.fmt_space(&base64.space1);
759
65
        self.buffer.push(';');
760
    }
761

            
762
245
    fn fmt_hex(&mut self, hex: &Hex) {
763
245
        self.buffer.push_str("hex,");
764
245
        self.fmt_space(&hex.space0);
765
245
        self.fmt_span("hex", &hex.encoded);
766
245
        self.fmt_space(&hex.space1);
767
245
        self.buffer.push(';');
768
    }
769

            
770
90
    fn fmt_regex(&mut self, regex: &Regex) {
771
90
        let s = str::replace(regex.inner.as_str(), "/", "\\/");
772
90
        let regex = format!("/{s}/");
773
90
        self.fmt_span("regex", &regex);
774
    }
775

            
776
5850
    fn fmt_template(&mut self, template: &Template) {
777
5850
        let s = template.to_encoded_string();
778
5850
        self.fmt_string(&escape_xml(&s));
779
    }
780

            
781
225
    fn fmt_placeholder(&mut self, placeholder: &Placeholder) {
782
225
        let placeholder = format!("{{{{{}}}}}", &placeholder.to_string());
783
225
        self.fmt_span("expr", &placeholder);
784
    }
785

            
786
770
    fn fmt_filter(&mut self, filter: &Filter) {
787
770
        self.fmt_filter_value(&filter.value);
788
    }
789

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

            
856
9800
    fn fmt_lts(&mut self, line_terminators: &[LineTerminator]) {
857
12685
        for line_terminator in line_terminators {
858
2885
            self.fmt_span_open("line");
859
2885
            if line_terminator.newline.value.is_empty() {
860
5
                self.buffer.push_str("<br />");
861
            }
862
2885
            self.fmt_span_close();
863
2885
            self.fmt_lt(line_terminator);
864
        }
865
    }
866
}
867

            
868
13880
fn escape_xml(s: &str) -> String {
869
13880
    s.replace('&', "&amp;")
870
13880
        .replace('<', "&lt;")
871
13880
        .replace('>', "&gt;")
872
}
873

            
874
impl Template {
875
7590
    fn to_encoded_string(&self) -> String {
876
7590
        let mut s = String::new();
877
7590
        if let Some(d) = self.delimiter {
878
3800
            s.push(d);
879
        }
880
7600
        for element in self.elements.iter() {
881
7600
            let elem_str = match element {
882
7390
                TemplateElement::String { encoded, .. } => encoded.to_string(),
883
210
                TemplateElement::Placeholder(expr) => format!("{{{{{expr}}}}}"),
884
            };
885
7600
            s.push_str(elem_str.as_str());
886
        }
887
7590
        if let Some(d) = self.delimiter {
888
3800
            s.push(d);
889
        }
890
7590
        s
891
    }
892
}
893

            
894
impl MultilineStringKind {
895
140
    fn to_encoded_string(&self) -> String {
896
140
        match self {
897
55
            MultilineStringKind::Text(text)
898
25
            | MultilineStringKind::Json(text)
899
100
            | MultilineStringKind::Xml(text) => text.value.to_encoded_string(),
900
40
            MultilineStringKind::GraphQl(graphql) => graphql.to_encoded_string(),
901
        }
902
    }
903
}
904

            
905
impl GraphQl {
906
40
    fn to_encoded_string(&self) -> String {
907
40
        let mut s = self.value.to_encoded_string();
908
40
        if let Some(vars) = &self.variables {
909
10
            s.push_str(&vars.to_encoded_string());
910
        }
911
40
        s
912
    }
913
}
914

            
915
impl GraphQlVariables {
916
10
    fn to_encoded_string(&self) -> String {
917
10
        let mut s = "variables".to_string();
918
10
        s.push_str(&self.space.value);
919
10
        s.push_str(&self.value.encoded());
920
10
        s.push_str(&self.whitespace.value);
921
10
        s
922
    }
923
}
924

            
925
2775
fn encode_html(s: String) -> String {
926
2775
    s.replace('>', "&gt;").replace('<', "&lt;")
927
}
928

            
929
260
fn format_multilines(s: &str) -> String {
930
260
    regex::Regex::new(r"\n|\r\n")
931
260
        .unwrap()
932
260
        .split(s)
933
5197
        .map(|l| format!("<span class=\"line\">{}</span>", escape_xml(l)))
934
260
        .collect::<Vec<String>>()
935
260
        .join("\n")
936
}
937

            
938
45
fn pop_str(string: &mut String, suffix: &str) {
939
45
    let len = string.len();
940
45
    let n = suffix.len();
941
45
    let len = len - n;
942
45
    string.truncate(len);
943
}
944

            
945
#[cfg(test)]
946
mod tests {
947
    use super::*;
948
    use crate::reader::Pos;
949

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

            
994
        let mut fmt = HtmlFormatter::new();
995
        fmt.fmt_multiline_string(&multiline_string, false);
996
        assert_eq!(
997
            fmt.buffer,
998
            "<span class=\"multiline\">```</span>\
999
        </span>\n\
        <span class=\"multiline\">\
            <span class=\"line\">line1</span>\n\
            <span class=\"line\">line2</span>\n\
            <span class=\"line\">```</span>"
        );
    }
    #[test]
    fn test_multilines() {
        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;"
        );
    }
}