1
/*
2
 * Hurl (https://hurl.dev)
3
 * Copyright (C) 2025 Orange
4
 *
5
 * Licensed under the Apache License, Version 2.0 (the "License");
6
 * you may not use this file except in compliance with the License.
7
 * You may obtain a copy of the License at
8
 *
9
 *          http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 *
17
 */
18
use crate::ast::{
19
    Assert, Base64, Body, BooleanOption, Bytes, Capture, CertificateAttributeName, Comment, Cookie,
20
    CookieAttribute, CookiePath, CountOption, DurationOption, Entry, EntryOption, File, FileParam,
21
    FileValue, Filter, FilterValue, GraphQl, GraphQlVariables, Hex, HurlFile, JsonValue, KeyValue,
22
    LineTerminator, Method, MultilineString, MultilineStringKind, MultipartParam, NaturalOption,
23
    OptionKind, Placeholder, Predicate, PredicateFunc, PredicateFuncValue, PredicateValue, Query,
24
    QueryValue, Regex, RegexValue, Request, Response, Section, SectionValue, Status, Template,
25
    TemplateElement, VariableDefinition, VariableValue, Version, Whitespace,
26
};
27
use crate::typing::Count;
28
use std::fmt::Display;
29

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

            
60
495
pub fn hurl_css() -> String {
61
495
    include_str!("hurl.css").to_string()
62
}
63

            
64
/// A HTML formatter for Hurl content.
65
struct HtmlFormatter {
66
    buffer: String,
67
}
68

            
69
impl HtmlFormatter {
70
590
    pub fn new() -> Self {
71
590
        HtmlFormatter {
72
590
            buffer: String::new(),
73
        }
74
    }
75

            
76
590
    pub fn fmt_hurl_file(&mut self, hurl_file: &HurlFile) -> &str {
77
590
        self.buffer.clear();
78
590
        self.fmt_pre_open("language-hurl");
79
1678
        hurl_file.entries.iter().for_each(|e| self.fmt_entry(e));
80
590
        self.fmt_lts(&hurl_file.line_terminators);
81
590
        self.fmt_pre_close();
82
590
        &self.buffer
83
    }
84

            
85
590
    fn fmt_pre_open(&mut self, class: &str) {
86
590
        self.buffer.push_str("<pre><code class=\"");
87
590
        self.buffer.push_str(class);
88
590
        self.buffer.push_str("\">");
89
    }
90

            
91
590
    fn fmt_pre_close(&mut self) {
92
590
        self.buffer.push_str("</code></pre>");
93
    }
94

            
95
19915
    fn fmt_span_open(&mut self, class: &str) {
96
19915
        self.buffer.push_str("<span class=\"");
97
19915
        self.buffer.push_str(class);
98
19915
        self.buffer.push_str("\">");
99
    }
100

            
101
19960
    fn fmt_span_close(&mut self) {
102
19960
        self.buffer.push_str("</span>");
103
    }
104

            
105
21910
    fn fmt_span(&mut self, class: &str, value: &str) {
106
21910
        self.buffer.push_str("<span class=\"");
107
21910
        self.buffer.push_str(class);
108
21910
        self.buffer.push_str("\">");
109
21910
        self.buffer.push_str(value);
110
21910
        self.buffer.push_str("</span>");
111
    }
112

            
113
1560
    fn fmt_entry(&mut self, entry: &Entry) {
114
1560
        self.fmt_span_open("hurl-entry");
115
1560
        self.fmt_request(&entry.request);
116
1560
        if let Some(response) = &entry.response {
117
1360
            self.fmt_response(response);
118
        }
119
1560
        self.fmt_span_close();
120
    }
121

            
122
1560
    fn fmt_request(&mut self, request: &Request) {
123
1560
        self.fmt_span_open("request");
124
1560
        self.fmt_lts(&request.line_terminators);
125
1560
        self.fmt_span_open("line");
126
1560
        self.fmt_space(&request.space0);
127
1560
        self.fmt_method(&request.method);
128
1560
        self.fmt_space(&request.space1);
129
1560
        let url = escape_xml(&request.url.to_encoded_string());
130
1560
        self.fmt_span("url", &url);
131
1560
        self.fmt_span_close();
132
1560
        self.fmt_lt(&request.line_terminator0);
133
1621
        request.headers.iter().for_each(|h| self.fmt_kv(h));
134
1650
        request.sections.iter().for_each(|s| self.fmt_section(s));
135
1560
        if let Some(body) = &request.body {
136
205
            self.fmt_body(body);
137
        }
138
1560
        self.fmt_span_close();
139
    }
140

            
141
1360
    fn fmt_response(&mut self, response: &Response) {
142
1360
        self.fmt_span_open("response");
143
1360
        self.fmt_lts(&response.line_terminators);
144
1360
        self.fmt_span_open("line");
145
1360
        self.fmt_space(&response.space0);
146
1360
        self.fmt_version(&response.version);
147
1360
        self.fmt_space(&response.space1);
148
1360
        self.fmt_status(&response.status);
149
1360
        self.fmt_span_close();
150
1360
        self.fmt_lt(&response.line_terminator0);
151
1417
        response.headers.iter().for_each(|h| self.fmt_kv(h));
152
1504
        response.sections.iter().for_each(|s| self.fmt_section(s));
153
1360
        if let Some(body) = &response.body {
154
405
            self.fmt_body(body);
155
        }
156
1360
        self.fmt_span_close();
157
    }
158

            
159
1560
    fn fmt_method(&mut self, method: &Method) {
160
1560
        self.fmt_span("method", &method.to_string());
161
    }
162

            
163
1360
    fn fmt_version(&mut self, version: &Version) {
164
1360
        self.fmt_span("version", &version.value.to_string());
165
    }
166

            
167
1360
    fn fmt_status(&mut self, status: &Status) {
168
1360
        self.fmt_number(status.value.to_string());
169
    }
170

            
171
1170
    fn fmt_section(&mut self, section: &Section) {
172
1170
        self.fmt_lts(&section.line_terminators);
173
1170
        self.fmt_space(&section.space0);
174
1170
        self.fmt_span_open("line");
175
1170
        let name = format!("[{}]", section.name());
176
1170
        self.fmt_span("section-header", &name);
177
1170
        self.fmt_span_close();
178
1170
        self.fmt_lt(&section.line_terminator0);
179
1170
        self.fmt_section_value(&section.value);
180
    }
181

            
182
1170
    fn fmt_section_value(&mut self, section_value: &SectionValue) {
183
1170
        match section_value {
184
2969
            SectionValue::Asserts(items) => items.iter().for_each(|item| self.fmt_assert(item)),
185
189
            SectionValue::QueryParams(items, _) => items.iter().for_each(|item| self.fmt_kv(item)),
186
15
            SectionValue::BasicAuth(item) => {
187
15
                if let Some(kv) = item {
188
15
                    self.fmt_kv(kv);
189
                }
190
            }
191
84
            SectionValue::FormParams(items, _) => items.iter().for_each(|item| self.fmt_kv(item)),
192
25
            SectionValue::MultipartFormData(items, _) => {
193
70
                items.iter().for_each(|item| self.fmt_multipart_param(item));
194
            }
195
18
            SectionValue::Cookies(items) => items.iter().for_each(|item| self.fmt_cookie(item)),
196
265
            SectionValue::Captures(items) => items.iter().for_each(|item| self.fmt_capture(item)),
197
305
            SectionValue::Options(items) => {
198
861
                items.iter().for_each(|item| self.fmt_entry_option(item));
199
            }
200
        }
201
    }
202

            
203
880
    fn fmt_kv(&mut self, kv: &KeyValue) {
204
880
        self.fmt_lts(&kv.line_terminators);
205
880
        self.fmt_span_open("line");
206
880
        self.fmt_space(&kv.space0);
207
880
        self.fmt_template(&kv.key);
208
880
        self.fmt_space(&kv.space1);
209
880
        self.buffer.push(':');
210
880
        self.fmt_space(&kv.space2);
211
880
        self.fmt_template(&kv.value);
212
880
        self.fmt_span_close();
213
880
        self.fmt_lt(&kv.line_terminator0);
214
    }
215

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

            
266
70
    fn fmt_count_option(&mut self, count_option: &CountOption) {
267
70
        match count_option {
268
50
            CountOption::Literal(repeat) => self.fmt_count(*repeat),
269
20
            CountOption::Placeholder(placeholder) => self.fmt_placeholder(placeholder),
270
        }
271
    }
272

            
273
50
    fn fmt_count(&mut self, count: Count) {
274
50
        match count {
275
40
            Count::Finite(n) => self.fmt_number(n),
276
10
            Count::Infinite => self.fmt_number(-1),
277
        };
278
    }
279

            
280
100
    fn fmt_variable_definition(&mut self, option: &VariableDefinition) {
281
100
        self.buffer.push_str(option.name.as_str());
282
100
        self.fmt_space(&option.space0);
283
100
        self.buffer.push('=');
284
100
        self.fmt_space(&option.space1);
285
100
        self.fmt_variable_value(&option.value);
286
    }
287

            
288
100
    fn fmt_variable_value(&mut self, option: &VariableValue) {
289
100
        match option {
290
5
            VariableValue::Null => self.fmt_span("null", "null"),
291
5
            VariableValue::Bool(v) => self.fmt_bool(*v),
292
40
            VariableValue::Number(v) => self.fmt_number(v),
293
50
            VariableValue::String(t) => self.fmt_template(t),
294
        }
295
    }
296

            
297
65
    fn fmt_multipart_param(&mut self, param: &MultipartParam) {
298
65
        match param {
299
20
            MultipartParam::Param(param) => self.fmt_kv(param),
300
45
            MultipartParam::FileParam(param) => self.fmt_file_param(param),
301
        };
302
    }
303

            
304
45
    fn fmt_file_param(&mut self, param: &FileParam) {
305
45
        self.fmt_lts(&param.line_terminators);
306
45
        self.fmt_span_open("line");
307
45
        self.fmt_space(&param.space0);
308
45
        self.fmt_template(&param.key);
309
45
        self.fmt_space(&param.space1);
310
45
        self.buffer.push(':');
311
45
        self.fmt_space(&param.space2);
312
45
        self.fmt_file_value(&param.value);
313
45
        self.fmt_span_close();
314
45
        self.fmt_lt(&param.line_terminator0);
315
    }
316

            
317
45
    fn fmt_file_value(&mut self, file_value: &FileValue) {
318
45
        self.buffer.push_str("file,");
319
45
        self.fmt_space(&file_value.space0);
320
45
        self.fmt_filename(&file_value.filename);
321
45
        self.fmt_space(&file_value.space1);
322
45
        self.buffer.push(';');
323
45
        self.fmt_space(&file_value.space2);
324
45
        if let Some(content_type) = &file_value.content_type {
325
15
            self.fmt_string(content_type);
326
        }
327
    }
328

            
329
185
    fn fmt_filename(&mut self, filename: &Template) {
330
185
        self.fmt_span_open("filename");
331
185
        let s = filename.to_string().replace(' ', "\\ ");
332
185
        self.buffer.push_str(s.as_str());
333
185
        self.fmt_span_close();
334
    }
335

            
336
15
    fn fmt_cookie(&mut self, cookie: &Cookie) {
337
15
        self.fmt_lts(&cookie.line_terminators);
338
15
        self.fmt_span_open("line");
339
15
        self.fmt_space(&cookie.space0);
340
15
        self.fmt_template(&cookie.name);
341
15
        self.fmt_space(&cookie.space1);
342
15
        self.buffer.push(':');
343
15
        self.fmt_space(&cookie.space2);
344
15
        self.fmt_template(&cookie.value);
345
15
        self.fmt_span_close();
346
15
        self.fmt_lt(&cookie.line_terminator0);
347
    }
348

            
349
245
    fn fmt_capture(&mut self, capture: &Capture) {
350
245
        self.fmt_lts(&capture.line_terminators);
351
245
        self.fmt_span_open("line");
352
245
        self.fmt_space(&capture.space0);
353
245
        self.fmt_template(&capture.name);
354
245
        self.fmt_space(&capture.space1);
355
245
        self.buffer.push(':');
356
245
        self.fmt_space(&capture.space2);
357
245
        self.fmt_query(&capture.query);
358
245
        for (space, filter) in capture.filters.iter() {
359
40
            self.fmt_space(space);
360
40
            self.fmt_filter(filter);
361
        }
362
245
        if capture.redact {
363
10
            self.fmt_space(&capture.space3);
364
10
            self.fmt_string("redact");
365
        }
366
245
        self.fmt_span_close();
367
245
        self.fmt_lt(&capture.line_terminator0);
368
    }
369

            
370
3090
    fn fmt_query(&mut self, query: &Query) {
371
3090
        self.fmt_query_value(&query.value);
372
    }
373

            
374
3090
    fn fmt_query_value(&mut self, query_value: &QueryValue) {
375
3090
        let query_type = query_value.identifier();
376
3090
        self.fmt_span("query-type", query_type);
377
3090
        match query_value {
378
305
            QueryValue::Header { space0, name } => {
379
305
                self.fmt_space(space0);
380
305
                self.fmt_template(name);
381
            }
382
75
            QueryValue::Cookie { space0, expr } => {
383
75
                self.fmt_space(space0);
384
75
                self.fmt_cookie_path(expr);
385
            }
386
145
            QueryValue::Xpath { space0, expr } => {
387
145
                self.fmt_space(space0);
388
145
                self.fmt_template(expr);
389
            }
390
1605
            QueryValue::Jsonpath { space0, expr } => {
391
1605
                self.fmt_space(space0);
392
1605
                self.fmt_template(expr);
393
            }
394
30
            QueryValue::Regex { space0, value } => {
395
30
                self.fmt_space(space0);
396
30
                self.fmt_regex_value(value);
397
            }
398
225
            QueryValue::Variable { space0, name } => {
399
225
                self.fmt_space(space0);
400
225
                self.fmt_template(name);
401
            }
402
            QueryValue::Certificate {
403
50
                space0,
404
50
                attribute_name: field,
405
50
            } => {
406
50
                self.fmt_space(space0);
407
50
                self.fmt_certificate_attribute_name(field);
408
            }
409
            QueryValue::Status
410
            | QueryValue::Url
411
            | QueryValue::Body
412
            | QueryValue::Duration
413
            | QueryValue::Bytes
414
            | QueryValue::Sha256
415
655
            | QueryValue::Md5 => {}
416
        }
417
    }
418

            
419
105
    fn fmt_regex_value(&mut self, regex_value: &RegexValue) {
420
105
        match regex_value {
421
65
            RegexValue::Template(template) => self.fmt_template(template),
422
40
            RegexValue::Regex(regex) => self.fmt_regex(regex),
423
        }
424
    }
425

            
426
75
    fn fmt_cookie_path(&mut self, cookie_path: &CookiePath) {
427
75
        self.fmt_span_open("string");
428
75
        self.buffer.push('"');
429
75
        self.buffer
430
75
            .push_str(cookie_path.name.to_encoded_string().as_str());
431
75
        if let Some(attribute) = &cookie_path.attribute {
432
60
            self.buffer.push('[');
433
60
            self.fmt_cookie_attribute(attribute);
434
60
            self.buffer.push(']');
435
        }
436
75
        self.buffer.push('"');
437
75
        self.fmt_span_close();
438
    }
439

            
440
60
    fn fmt_cookie_attribute(&mut self, cookie_attribute: &CookieAttribute) {
441
60
        self.fmt_space(&cookie_attribute.space0);
442
60
        self.buffer.push_str(cookie_attribute.name.value().as_str());
443
60
        self.fmt_space(&cookie_attribute.space1);
444
    }
445

            
446
50
    fn fmt_certificate_attribute_name(&mut self, name: &CertificateAttributeName) {
447
50
        self.fmt_span_open("string");
448
50
        self.buffer.push('"');
449
50
        self.buffer.push_str(name.identifier());
450
50
        self.buffer.push('"');
451
50
        self.fmt_span_close();
452
    }
453

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

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

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

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

            
486
2845
        match value {
487
1795
            PredicateFuncValue::Equal { space0, value, .. } => {
488
1795
                self.fmt_space(space0);
489
1795
                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
130
            PredicateFuncValue::StartWith { space0, value } => {
512
130
                self.fmt_space(space0);
513
130
                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
95
            PredicateFuncValue::Match { space0, value } => {
528
95
                self.fmt_space(space0);
529
95
                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
2415
    fn fmt_predicate_value(&mut self, predicate_value: &PredicateValue) {
545
2415
        match predicate_value {
546
1115
            PredicateValue::String(value) => self.fmt_template(value),
547
45
            PredicateValue::MultilineString(value) => self.fmt_multiline_string(value, false),
548
735
            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
250
            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
60
            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
610
    fn fmt_body(&mut self, body: &Body) {
644
610
        self.fmt_lts(&body.line_terminators);
645
610
        self.fmt_space(&body.space0);
646
610
        self.fmt_bytes(&body.value);
647
610
        self.fmt_lt(&body.line_terminator0);
648
    }
649

            
650
610
    fn fmt_bytes(&mut self, bytes: &Bytes) {
651
610
        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
230
            Bytes::OnelineString(value) => {
668
230
                self.fmt_span_open("line");
669
230
                self.fmt_template(value);
670
230
                self.fmt_span_close();
671
            }
672
60
            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
6935
    fn fmt_string(&mut self, value: &str) {
679
6935
        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
415
    fn fmt_bool_option(&mut self, value: &BooleanOption) {
687
415
        match value {
688
330
            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(literal) => {
703
45
                self.fmt_span("number", &literal.value.to_string());
704
45
                if let Some(unit) = literal.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
2300
    fn fmt_number<T: Sized + Display>(&mut self, value: T) {
713
2300
        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
60
    fn fmt_json_value(&mut self, json_value: &JsonValue) {
722
60
        let json = format_multilines(&json_value.encoded());
723
60
        self.fmt_span("json", &json);
724
    }
725

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

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

            
741
1440
    fn fmt_comment(&mut self, comment: &Comment) {
742
1440
        let comment = format!("#{}", escape_xml(&comment.value));
743
1440
        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
280
    fn fmt_hex(&mut self, hex: &Hex) {
763
280
        self.buffer.push_str("hex,");
764
280
        self.fmt_space(&hex.space0);
765
280
        self.fmt_span("hex", &hex.encoded);
766
280
        self.fmt_space(&hex.space1);
767
280
        self.buffer.push(';');
768
    }
769

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

            
776
6110
    fn fmt_template(&mut self, template: &Template) {
777
6110
        let s = template.to_encoded_string();
778
6110
        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
810
    fn fmt_filter(&mut self, filter: &Filter) {
787
810
        self.fmt_filter_value(&filter.value);
788
    }
789

            
790
810
    fn fmt_filter_value(&mut self, filter_value: &FilterValue) {
791
810
        self.fmt_span("filter-type", filter_value.identifier());
792
810
        match filter_value {
793
45
            FilterValue::Decode { space0, encoding } => {
794
45
                self.fmt_space(space0);
795
45
                self.fmt_template(encoding);
796
            }
797
35
            FilterValue::Format { space0, fmt } => {
798
35
                self.fmt_space(space0);
799
35
                self.fmt_template(fmt);
800
            }
801
20
            FilterValue::JsonPath { space0, expr } => {
802
20
                self.fmt_space(space0);
803
20
                self.fmt_template(expr);
804
            }
805
115
            FilterValue::Nth { space0, n: value } => {
806
115
                self.fmt_space(space0);
807
115
                self.fmt_number(value);
808
            }
809
40
            FilterValue::Regex { space0, value } => {
810
40
                self.fmt_space(space0);
811
40
                self.fmt_regex_value(value);
812
            }
813
            FilterValue::Replace {
814
35
                space0,
815
35
                old_value,
816
35
                space1,
817
35
                new_value,
818
35
            } => {
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_space(space0);
826
15
                self.fmt_template(sep);
827
            }
828
30
            FilterValue::ToDate { space0, fmt } => {
829
30
                self.fmt_space(space0);
830
30
                self.fmt_template(fmt);
831
            }
832
20
            FilterValue::XPath { space0, expr } => {
833
20
                self.fmt_space(space0);
834
20
                self.fmt_template(expr);
835
            }
836
            FilterValue::Base64Decode
837
            | FilterValue::Base64Encode
838
            | FilterValue::Count
839
            | FilterValue::DaysAfterNow
840
            | FilterValue::DaysBeforeNow
841
            | FilterValue::HtmlEscape
842
            | FilterValue::HtmlUnescape
843
            | FilterValue::ToFloat
844
            | FilterValue::ToInt
845
            | FilterValue::UrlDecode
846
455
            | FilterValue::UrlEncode => {}
847
        };
848
    }
849

            
850
10120
    fn fmt_lts(&mut self, line_terminators: &[LineTerminator]) {
851
13095
        for line_terminator in line_terminators {
852
2975
            self.fmt_span_open("line");
853
2975
            if line_terminator.newline.value.is_empty() {
854
5
                self.buffer.push_str("<br />");
855
            }
856
2975
            self.fmt_span_close();
857
2975
            self.fmt_lt(line_terminator);
858
        }
859
    }
860
}
861

            
862
14280
fn escape_xml(s: &str) -> String {
863
14280
    s.replace('&', "&amp;")
864
14280
        .replace('<', "&lt;")
865
14280
        .replace('>', "&gt;")
866
}
867

            
868
impl Template {
869
7885
    fn to_encoded_string(&self) -> String {
870
7885
        let mut s = String::new();
871
7885
        if let Some(d) = self.delimiter {
872
3890
            s.push(d);
873
        }
874
7895
        for element in self.elements.iter() {
875
7895
            let elem_str = match element {
876
7655
                TemplateElement::String { encoded, .. } => encoded.to_string(),
877
240
                TemplateElement::Placeholder(expr) => format!("{{{{{expr}}}}}"),
878
            };
879
7895
            s.push_str(elem_str.as_str());
880
        }
881
7885
        if let Some(d) = self.delimiter {
882
3890
            s.push(d);
883
        }
884
7885
        s
885
    }
886
}
887

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

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

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

            
919
2845
fn encode_html(s: &str) -> String {
920
2845
    s.replace('>', "&gt;").replace('<', "&lt;")
921
}
922

            
923
270
fn format_multilines(s: &str) -> String {
924
270
    regex::Regex::new(r"\n|\r\n")
925
270
        .unwrap()
926
270
        .split(s)
927
5224
        .map(|l| format!("<span class=\"line\">{}</span>", escape_xml(l)))
928
270
        .collect::<Vec<String>>()
929
270
        .join("\n")
930
}
931

            
932
45
fn pop_str(string: &mut String, suffix: &str) {
933
45
    let len = string.len();
934
45
    let n = suffix.len();
935
45
    let len = len - n;
936
45
    string.truncate(len);
937
}
938

            
939
#[cfg(test)]
940
mod tests {
941
    use super::*;
942
    use crate::ast::{JsonObjectElement, SourceInfo, Text};
943
    use crate::reader::Pos;
944

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

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