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

            
20
use crate::ast::{
21
    Assert, Base64, Body, BooleanOption, Bytes, Capture, CertificateAttributeName, Comment, Cookie,
22
    CookieAttribute, CookiePath, CountOption, DurationOption, Entry, EntryOption, File, FileParam,
23
    FileValue, Filter, FilterValue, Hex, HurlFile, JsonValue, KeyValue, LineTerminator, Method,
24
    MultilineString, MultipartParam, NaturalOption, OptionKind, Placeholder, Predicate,
25
    PredicateFunc, PredicateFuncValue, PredicateValue, Query, QueryValue, Regex, RegexValue,
26
    Request, Response, Section, SectionValue, Status, Template, VariableDefinition, VariableValue,
27
    Version, Whitespace,
28
};
29
use crate::typing::{Count, ToSource};
30

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
471
2885
    fn fmt_predicate(&mut self, predicate: &Predicate) {
472
2885
        if predicate.not {
473
185
            self.fmt_span("not", "not");
474
185
            self.fmt_space(&predicate.space0);
475
        }
476
2885
        self.fmt_predicate_func(&predicate.predicate_func);
477
    }
478

            
479
2885
    fn fmt_predicate_func(&mut self, predicate_func: &PredicateFunc) {
480
2885
        self.fmt_predicate_func_value(&predicate_func.value);
481
    }
482

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

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

            
548
2445
    fn fmt_predicate_value(&mut self, predicate_value: &PredicateValue) {
549
2445
        match predicate_value {
550
1140
            PredicateValue::String(value) => self.fmt_template(value),
551
50
            PredicateValue::MultilineString(value) => self.fmt_multiline_string(value, false),
552
735
            PredicateValue::Number(value) => self.fmt_number(value.to_source()),
553
60
            PredicateValue::Bool(value) => self.fmt_bool(*value),
554
15
            PredicateValue::File(value) => self.fmt_file(value),
555
250
            PredicateValue::Hex(value) => self.fmt_hex(value),
556
10
            PredicateValue::Base64(value) => self.fmt_base64(value),
557
100
            PredicateValue::Placeholder(value) => self.fmt_placeholder(value),
558
25
            PredicateValue::Null => self.fmt_span("null", "null"),
559
60
            PredicateValue::Regex(value) => self.fmt_regex(value),
560
        };
561
    }
562

            
563
200
    fn fmt_multiline_string(&mut self, multiline_string: &MultilineString, as_body: bool) {
564
200
        let body = multiline_string.to_source();
565
200
        let mut body = format_lines(body.as_str(), true);
566
200
        if !as_body {
567
50
            // A multiline AST element spans multiple line. When used as an assert, the AST element in
568
50
            // in the middle of the current line:
569
50
            //
570
50
            // ~~~
571
50
            // GET https://foo.com
572
50
            // HTTP 200
573
50
            // [Asserts]
574
50
            // body == ```
575
50
            // line1
576
50
            // line2
577
50
            // ```
578
50
            // ~~~
579
50
            //
580
50
            // We don't want the multiline AST to begin a new `<span clas="line">`
581
50
            // element so we split the multiline AST element in a list of single-line "multiline"
582
50
            // elements. This way, each new multiline element is wrapped in a single line.
583
50
            // NOTE: this still feels hacky to me, I'm not sure that we should add span for lines, it
584
50
            // intermixes an AST hierarchical view and a line oriented HTML view.
585
50
            body = body
586
50
                .strip_prefix("<span class=\"line\">")
587
50
                .unwrap()
588
50
                .strip_suffix("</span>")
589
50
                .unwrap()
590
50
                .to_string();
591
        }
592
200
        self.buffer.push_str(&body);
593
    }
594

            
595
620
    fn fmt_body(&mut self, body: &Body) {
596
620
        self.fmt_lts(&body.line_terminators);
597
620
        self.fmt_space(&body.space0);
598
620
        self.fmt_bytes(&body.value);
599
620
        self.fmt_lt(&body.line_terminator0);
600
    }
601

            
602
620
    fn fmt_bytes(&mut self, bytes: &Bytes) {
603
620
        match bytes {
604
55
            Bytes::Base64(value) => {
605
55
                self.fmt_span_open("line");
606
55
                self.fmt_base64(value);
607
55
                self.fmt_span_close();
608
            }
609
70
            Bytes::File(value) => {
610
70
                self.fmt_span_open("line");
611
70
                self.fmt_file(value);
612
70
                self.fmt_span_close();
613
            }
614
30
            Bytes::Hex(value) => {
615
30
                self.fmt_span_open("line");
616
30
                self.fmt_hex(value);
617
30
                self.fmt_span_close();
618
            }
619
230
            Bytes::OnelineString(value) => {
620
230
                self.fmt_span_open("line");
621
230
                self.fmt_template(value);
622
230
                self.fmt_span_close();
623
            }
624
60
            Bytes::Json(value) => self.fmt_json_value(value),
625
150
            Bytes::MultilineString(value) => self.fmt_multiline_string(value, true),
626
25
            Bytes::Xml(value) => self.fmt_xml(value),
627
        }
628
    }
629

            
630
6975
    fn fmt_string(&mut self, value: &str) {
631
6975
        self.fmt_span("string", value);
632
    }
633

            
634
65
    fn fmt_bool(&mut self, value: bool) {
635
65
        self.fmt_span("boolean", &value.to_string());
636
    }
637

            
638
425
    fn fmt_bool_option(&mut self, value: &BooleanOption) {
639
425
        match value {
640
340
            BooleanOption::Literal(value) => self.fmt_span("boolean", &value.to_string()),
641
85
            BooleanOption::Placeholder(value) => self.fmt_placeholder(value),
642
        }
643
    }
644

            
645
10
    fn fmt_natural_option(&mut self, value: &NaturalOption) {
646
10
        match value {
647
5
            NaturalOption::Literal(value) => {
648
5
                self.fmt_span("number", &value.to_source().to_string());
649
            }
650
5
            NaturalOption::Placeholder(value) => self.fmt_placeholder(value),
651
        }
652
    }
653

            
654
60
    fn fmt_duration_option(&mut self, value: &DurationOption) {
655
60
        match value {
656
45
            DurationOption::Literal(literal) => {
657
45
                self.fmt_span("number", &literal.value.to_source().to_string());
658
45
                if let Some(unit) = literal.unit {
659
35
                    self.fmt_span("unit", &unit.to_string());
660
                }
661
            }
662
15
            DurationOption::Placeholder(value) => self.fmt_placeholder(value),
663
        }
664
    }
665

            
666
2310
    fn fmt_number<T: Sized + Display>(&mut self, value: T) {
667
2310
        self.fmt_span("number", &value.to_string());
668
    }
669

            
670
25
    fn fmt_xml(&mut self, value: &str) {
671
25
        let xml = format_lines(value, false);
672
25
        self.fmt_span("xml", &xml);
673
    }
674

            
675
60
    fn fmt_json_value(&mut self, json_value: &JsonValue) {
676
60
        let json = format_lines(json_value.to_source().as_str(), false);
677
60
        self.fmt_span("json", &json);
678
    }
679

            
680
39750
    fn fmt_space(&mut self, space: &Whitespace) {
681
39750
        let Whitespace { value, .. } = space;
682
39750
        if !value.is_empty() {
683
14575
            self.buffer.push_str(value);
684
25175
        };
685
    }
686

            
687
12650
    fn fmt_lt(&mut self, lt: &LineTerminator) {
688
12650
        self.fmt_space(&lt.space0);
689
12650
        if let Some(v) = &lt.comment {
690
1470
            self.fmt_comment(v);
691
        }
692
12650
        self.buffer.push_str(lt.newline.as_str());
693
    }
694

            
695
1470
    fn fmt_comment(&mut self, comment: &Comment) {
696
1470
        let comment = format!("#{}", escape_xml(&comment.value));
697
1470
        self.fmt_span("comment", &comment);
698
    }
699

            
700
85
    fn fmt_file(&mut self, file: &File) {
701
85
        self.buffer.push_str("file,");
702
85
        self.fmt_space(&file.space0);
703
85
        self.fmt_filename(&file.filename);
704
85
        self.fmt_space(&file.space1);
705
85
        self.buffer.push(';');
706
    }
707

            
708
65
    fn fmt_base64(&mut self, base64: &Base64) {
709
65
        self.buffer.push_str("base64,");
710
65
        self.fmt_space(&base64.space0);
711
65
        self.fmt_span("base64", &base64.source.to_string());
712
65
        self.fmt_space(&base64.space1);
713
65
        self.buffer.push(';');
714
    }
715

            
716
280
    fn fmt_hex(&mut self, hex: &Hex) {
717
280
        self.buffer.push_str("hex,");
718
280
        self.fmt_space(&hex.space0);
719
280
        self.fmt_span("hex", &hex.source.to_string());
720
280
        self.fmt_space(&hex.space1);
721
280
        self.buffer.push(';');
722
    }
723

            
724
100
    fn fmt_regex(&mut self, regex: &Regex) {
725
100
        let s = str::replace(regex.inner.as_str(), "/", "\\/");
726
100
        let regex = format!("/{s}/");
727
100
        self.fmt_span("regex", &regex);
728
    }
729

            
730
6155
    fn fmt_template(&mut self, template: &Template) {
731
6155
        let s = template.to_source();
732
6155
        self.fmt_string(&escape_xml(s.as_str()));
733
    }
734

            
735
225
    fn fmt_placeholder(&mut self, placeholder: &Placeholder) {
736
225
        let placeholder = placeholder.to_source();
737
225
        self.fmt_span("expr", placeholder.as_str());
738
    }
739

            
740
820
    fn fmt_filter(&mut self, filter: &Filter) {
741
820
        self.fmt_filter_value(&filter.value);
742
    }
743

            
744
820
    fn fmt_filter_value(&mut self, filter_value: &FilterValue) {
745
820
        self.fmt_span("filter-type", filter_value.identifier());
746
820
        match filter_value {
747
45
            FilterValue::Decode { space0, encoding } => {
748
45
                self.fmt_space(space0);
749
45
                self.fmt_template(encoding);
750
            }
751
35
            FilterValue::Format { space0, fmt } => {
752
35
                self.fmt_space(space0);
753
35
                self.fmt_template(fmt);
754
            }
755
20
            FilterValue::JsonPath { space0, expr } => {
756
20
                self.fmt_space(space0);
757
20
                self.fmt_template(expr);
758
            }
759
115
            FilterValue::Nth { space0, n: value } => {
760
115
                self.fmt_space(space0);
761
115
                self.fmt_number(value.to_source());
762
            }
763
40
            FilterValue::Regex { space0, value } => {
764
40
                self.fmt_space(space0);
765
40
                self.fmt_regex_value(value);
766
            }
767
            FilterValue::Replace {
768
35
                space0,
769
35
                old_value,
770
35
                space1,
771
35
                new_value,
772
35
            } => {
773
35
                self.fmt_space(space0);
774
35
                self.fmt_regex_value(old_value);
775
35
                self.fmt_space(space1);
776
35
                self.fmt_template(new_value);
777
            }
778
15
            FilterValue::Split { space0, sep } => {
779
15
                self.fmt_space(space0);
780
15
                self.fmt_template(sep);
781
            }
782
30
            FilterValue::ToDate { space0, fmt } => {
783
30
                self.fmt_space(space0);
784
30
                self.fmt_template(fmt);
785
            }
786
20
            FilterValue::XPath { space0, expr } => {
787
20
                self.fmt_space(space0);
788
20
                self.fmt_template(expr);
789
            }
790
            FilterValue::Base64Decode
791
            | FilterValue::Base64Encode
792
            | FilterValue::Count
793
            | FilterValue::DaysAfterNow
794
            | FilterValue::DaysBeforeNow
795
            | FilterValue::HtmlEscape
796
            | FilterValue::HtmlUnescape
797
            | FilterValue::ToFloat
798
            | FilterValue::ToInt
799
            | FilterValue::ToString
800
            | FilterValue::UrlDecode
801
465
            | FilterValue::UrlEncode => {}
802
        };
803
    }
804

            
805
10245
    fn fmt_lts(&mut self, line_terminators: &[LineTerminator]) {
806
13235
        for line_terminator in line_terminators {
807
2990
            self.fmt_span_open("line");
808
2990
            if line_terminator.newline.value.is_empty() {
809
5
                self.buffer.push_str("<br />");
810
            }
811
2990
            self.fmt_span_close();
812
2990
            self.fmt_lt(line_terminator);
813
        }
814
    }
815
}
816

            
817
14615
fn escape_xml(s: &str) -> String {
818
14615
    s.replace('&', "&amp;")
819
14615
        .replace('<', "&lt;")
820
14615
        .replace('>', "&gt;")
821
}
822

            
823
2885
fn encode_html(s: &str) -> String {
824
2885
    s.replace('>', "&gt;").replace('<', "&lt;")
825
}
826

            
827
285
fn format_lines(s: &str, use_multiline_class: bool) -> String {
828
285
    regex::Regex::new(r"\n|\r\n")
829
285
        .unwrap()
830
285
        .split(s)
831
5457
        .map(|l| {
832
5400
            let text = escape_xml(l);
833
5400
            if use_multiline_class {
834
1760
                format!("<span class=\"line\"><span class=\"multiline\">{text}</span></span>")
835
            } else {
836
3640
                format!("<span class=\"line\">{text}</span>")
837
            }
838
5457
        })
839
285
        .collect::<Vec<String>>()
840
285
        .join("\n")
841
}
842

            
843
#[cfg(test)]
844
mod tests {
845
    use super::*;
846
    use crate::ast::{JsonObjectElement, MultilineStringKind, SourceInfo, TemplateElement};
847
    use crate::reader::Pos;
848
    use crate::typing::ToSource;
849

            
850
    #[test]
851
    fn test_multiline_string() {
852
        // ```
853
        // line1
854
        // line2
855
        // ```
856
        let kind = MultilineStringKind::Text(Template {
857
            delimiter: None,
858
            elements: vec![TemplateElement::String {
859
                value: "line1\nline2\n".to_string(),
860
                source: "line1\nline2\n".to_source(),
861
            }],
862
            source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
863
        });
864
        let attributes = vec![];
865
        let multiline_string = MultilineString {
866
            attributes,
867
            space: Whitespace {
868
                value: String::new(),
869
                source_info: SourceInfo {
870
                    start: Pos { line: 1, column: 4 },
871
                    end: Pos { line: 1, column: 4 },
872
                },
873
            },
874
            newline: Whitespace {
875
                value: "\n".to_string(),
876
                source_info: SourceInfo {
877
                    start: Pos { line: 1, column: 4 },
878
                    end: Pos { line: 2, column: 1 },
879
                },
880
            },
881
            kind,
882
        };
883
        let mut fmt = HtmlFormatter::new();
884
        fmt.fmt_multiline_string(&multiline_string, true);
885
        assert_eq!(
886
            fmt.buffer,
887
            "<span class=\"line\">\
888
                <span class=\"multiline\">```</span>\
889
            </span>\n\
890
            <span class=\"line\">\
891
                <span class=\"multiline\">line1</span>\
892
            </span>\n\
893
            <span class=\"line\">\
894
                <span class=\"multiline\">line2</span>\
895
            </span>\n\
896
            <span class=\"line\">\
897
                <span class=\"multiline\">```</span>\
898
            </span>"
899
        );
900

            
901
        let mut fmt = HtmlFormatter::new();
902
        fmt.fmt_multiline_string(&multiline_string, false);
903
        assert_eq!(
904
            fmt.buffer,
905
            "<span class=\"multiline\">```</span>\
906
        </span>\n\
907
        <span class=\"line\">\
908
            <span class=\"multiline\">line1</span>\
909
        </span>\n\
910
        <span class=\"line\">\
911
            <span class=\"multiline\">line2</span>\
912
        </span>\n\
913
        <span class=\"line\">\
914
            <span class=\"multiline\">```</span>"
915
        );
916
    }
917

            
918
    #[test]
919
    fn test_multilines() {
920
        assert_eq!(
921
            format_lines("{\n   \"id\": 1\n}", false),
922
            "<span class=\"line\">{</span>\n\
923
            <span class=\"line\">   \"id\": 1</span>\n\
924
            <span class=\"line\">}</span>"
925
        );
926
        assert_eq!(
927
            format_lines("{\n   \"id\": 1\n}", true),
928
            "<span class=\"line\"><span class=\"multiline\">{</span></span>\n\
929
            <span class=\"line\"><span class=\"multiline\">   \"id\": 1</span></span>\n\
930
            <span class=\"line\"><span class=\"multiline\">}</span></span>"
931
        );
932

            
933
        assert_eq!(
934
            format_lines(
935
                "<?xml version=\"1.0\"?>\n\
936
            <drink>café</drink>",
937
                false
938
            ),
939
            "<span class=\"line\">&lt;?xml version=\"1.0\"?&gt;</span>\n\
940
            <span class=\"line\">&lt;drink&gt;café&lt;/drink&gt;</span>"
941
        );
942

            
943
        assert_eq!(
944
            format_lines("Hello\n", false),
945
            "<span class=\"line\">Hello</span>\n\
946
            <span class=\"line\"></span>"
947
        );
948
    }
949

            
950
    #[test]
951
    fn test_json() {
952
        let mut fmt = HtmlFormatter::new();
953
        let value = JsonValue::Object {
954
            space0: String::new(),
955
            elements: vec![JsonObjectElement {
956
                space0: "\n   ".to_string(),
957
                name: Template::new(
958
                    Some('"'),
959
                    vec![TemplateElement::String {
960
                        value: "id".to_string(),
961
                        source: "id".to_source(),
962
                    }],
963
                    SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
964
                ),
965
                space1: String::new(),
966
                space2: " ".to_string(),
967
                value: JsonValue::Number("1".to_string()),
968
                space3: "\n".to_string(),
969
            }],
970
        };
971
        fmt.fmt_json_value(&value);
972
        assert_eq!(
973
            fmt.buffer,
974
            "<span class=\"json\">\
975
                <span class=\"line\">{</span>\n\
976
                <span class=\"line\">   \"id\": 1</span>\n\
977
                <span class=\"line\">}</span>\
978
            </span>"
979
        );
980
    }
981

            
982
    #[test]
983
    fn test_json_encoded_newline() {
984
        let mut fmt = HtmlFormatter::new();
985
        let value = JsonValue::String(Template::new(
986
            Some('"'),
987
            vec![TemplateElement::String {
988
                value: "\n".to_string(),
989
                source: "\\n".to_source(),
990
            }],
991
            SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
992
        ));
993
        fmt.fmt_json_value(&value);
994
        assert_eq!(
995
            fmt.buffer,
996
            "<span class=\"json\"><span class=\"line\">\"\\n\"</span></span>"
997
        );
998
    }
999

            
    #[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;"
        );
    }
}