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,
23
    FilenameParam, FilenameValue, Filter, FilterValue, Hex, HurlFile, JsonValue, KeyValue,
24
    LineTerminator, Method, MultilineString, MultipartParam, NaturalOption, OptionKind,
25
    Placeholder, Predicate, PredicateFunc, PredicateFuncValue, PredicateValue, Query, QueryValue,
26
    Regex, RegexValue, Request, Response, Section, SectionValue, Status, Template,
27
    VariableDefinition, VariableValue, 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
565
pub fn format(hurl_file: &HurlFile, standalone: bool) -> String {
36
565
    let mut fmt = HtmlFormatter::new();
37
565
    let body = fmt.fmt_hurl_file(hurl_file);
38
565
    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
560
        body.to_string()
58
    }
59
}
60

            
61
470
pub fn hurl_css() -> String {
62
470
    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
565
    pub fn new() -> Self {
72
565
        HtmlFormatter {
73
565
            buffer: String::new(),
74
        }
75
    }
76

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
263
75
    fn fmt_count_option(&mut self, count_option: &CountOption) {
264
75
        match count_option {
265
55
            CountOption::Literal(repeat) => self.fmt_count(*repeat),
266
20
            CountOption::Placeholder(placeholder) => self.fmt_placeholder(placeholder),
267
        }
268
    }
269

            
270
55
    fn fmt_count(&mut self, count: Count) {
271
55
        match count {
272
45
            Count::Finite(n) => self.fmt_number(n),
273
10
            Count::Infinite => self.fmt_number(-1),
274
        };
275
    }
276

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

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

            
294
65
    fn fmt_multipart_param(&mut self, param: &MultipartParam) {
295
65
        match param {
296
20
            MultipartParam::Param(param) => self.fmt_kv(param),
297
45
            MultipartParam::FilenameParam(param) => self.fmt_file_param(param),
298
        };
299
    }
300

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

            
313
45
    fn fmt_file_value(&mut self, file_value: &FilenameValue) {
314
45
        self.buffer.push_str("file,");
315
45
        self.fmt_space(&file_value.space0);
316
45
        self.fmt_filename(&file_value.filename);
317
45
        self.fmt_space(&file_value.space1);
318
45
        self.buffer.push(';');
319
45
        self.fmt_space(&file_value.space2);
320
45
        if let Some(content_type) = &file_value.content_type {
321
15
            self.fmt_template(content_type);
322
        }
323
    }
324

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

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

            
344
270
    fn fmt_capture(&mut self, capture: &Capture) {
345
270
        self.fmt_lts(&capture.line_terminators);
346
270
        self.fmt_span_open("line");
347
270
        self.fmt_space(&capture.space0);
348
270
        self.fmt_template(&capture.name);
349
270
        self.fmt_space(&capture.space1);
350
270
        self.buffer.push(':');
351
270
        self.fmt_space(&capture.space2);
352
270
        self.fmt_query(&capture.query);
353
270
        for (space, filter) in capture.filters.iter() {
354
50
            self.fmt_space(space);
355
50
            self.fmt_filter(filter);
356
        }
357
270
        self.fmt_space(&capture.space3);
358
270
        if capture.redact {
359
20
            self.fmt_string("redact");
360
        }
361
270
        self.fmt_lt(&capture.line_terminator0);
362
    }
363

            
364
3365
    fn fmt_query(&mut self, query: &Query) {
365
3365
        self.fmt_query_value(&query.value);
366
    }
367

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

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

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

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

            
442
50
    fn fmt_certificate_attribute_name(&mut self, name: &CertificateAttributeName) {
443
50
        self.fmt_span_open("string");
444
50
        self.buffer.push('"');
445
50
        self.buffer.push_str(name.identifier());
446
50
        self.buffer.push('"');
447
50
        self.fmt_span_close();
448
    }
449

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

            
464
3095
    fn fmt_predicate(&mut self, predicate: &Predicate) {
465
3095
        if predicate.not {
466
195
            self.fmt_span("not", "not");
467
195
            self.fmt_space(&predicate.space0);
468
        }
469
3095
        self.fmt_predicate_func(&predicate.predicate_func);
470
    }
471

            
472
3095
    fn fmt_predicate_func(&mut self, predicate_func: &PredicateFunc) {
473
3095
        self.fmt_predicate_func_value(&predicate_func.value);
474
    }
475

            
476
3095
    fn fmt_predicate_func_value(&mut self, value: &PredicateFuncValue) {
477
3095
        self.fmt_span_open("predicate-type");
478
3095
        self.buffer.push_str(&encode_html(value.identifier()));
479
3095
        self.fmt_span_close();
480
3095

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

            
541
2595
    fn fmt_predicate_value(&mut self, predicate_value: &PredicateValue) {
542
2595
        match predicate_value {
543
1210
            PredicateValue::String(value) => self.fmt_template(value),
544
50
            PredicateValue::MultilineString(value) => self.fmt_multiline_string(value, false),
545
755
            PredicateValue::Number(value) => self.fmt_number(value.to_source()),
546
60
            PredicateValue::Bool(value) => self.fmt_bool(*value),
547
15
            PredicateValue::File(value) => self.fmt_file(value),
548
280
            PredicateValue::Hex(value) => self.fmt_hex(value),
549
10
            PredicateValue::Base64(value) => self.fmt_base64(value),
550
130
            PredicateValue::Placeholder(value) => self.fmt_placeholder(value),
551
25
            PredicateValue::Null => self.fmt_span("null", "null"),
552
60
            PredicateValue::Regex(value) => self.fmt_regex(value),
553
        };
554
    }
555

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

            
588
595
    fn fmt_body(&mut self, body: &Body) {
589
595
        self.fmt_lts(&body.line_terminators);
590
595
        self.fmt_space(&body.space0);
591
595
        self.fmt_bytes(&body.value);
592
595
        let lt = &body.line_terminator0;
593
595
        self.fmt_space(&lt.space0);
594
595
        if let Some(v) = &lt.comment {
595
15
            self.fmt_comment(v);
596
        }
597
595
        self.buffer.push_str(lt.newline.as_str());
598
    }
599

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

            
628
7350
    fn fmt_string(&mut self, value: &str) {
629
7350
        self.fmt_span("string", value);
630
    }
631

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

            
636
430
    fn fmt_bool_option(&mut self, value: &BooleanOption) {
637
430
        match value {
638
345
            BooleanOption::Literal(value) => self.fmt_span("boolean", &value.to_string()),
639
85
            BooleanOption::Placeholder(value) => self.fmt_placeholder(value),
640
        }
641
    }
642

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

            
652
70
    fn fmt_duration_option(&mut self, value: &DurationOption) {
653
70
        match value {
654
50
            DurationOption::Literal(literal) => {
655
50
                self.fmt_span("number", &literal.value.to_source().to_string());
656
50
                if let Some(unit) = literal.unit {
657
40
                    self.fmt_span("unit", &unit.to_string());
658
                }
659
            }
660
20
            DurationOption::Placeholder(value) => self.fmt_placeholder(value),
661
        }
662
    }
663

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

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

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

            
678
41350
    fn fmt_space(&mut self, space: &Whitespace) {
679
41350
        let Whitespace { value, .. } = space;
680
41350
        if !value.is_empty() {
681
15440
            self.buffer.push_str(value);
682
25910
        };
683
    }
684

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

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

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

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

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

            
723
100
    fn fmt_regex(&mut self, regex: &Regex) {
724
100
        self.fmt_span("regex", regex.to_source().as_str());
725
    }
726

            
727
6500
    fn fmt_template(&mut self, template: &Template) {
728
6500
        let s = template.to_source();
729
6500
        self.fmt_string(&escape_xml(s.as_str()));
730
    }
731

            
732
260
    fn fmt_placeholder(&mut self, placeholder: &Placeholder) {
733
260
        let placeholder = placeholder.to_source();
734
260
        self.fmt_span("expr", placeholder.as_str());
735
    }
736

            
737
985
    fn fmt_filter(&mut self, filter: &Filter) {
738
985
        self.fmt_filter_value(&filter.value);
739
    }
740

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

            
810
10425
    fn fmt_lts(&mut self, line_terminators: &[LineTerminator]) {
811
13425
        for lt in line_terminators {
812
3000
            self.fmt_span_open("line");
813
3000
            self.fmt_space(&lt.space0);
814
3000
            if let Some(v) = &lt.comment {
815
1045
                self.fmt_comment(v);
816
            }
817
3000
            self.fmt_span_close();
818
3000
            if !lt.newline.value.is_empty() {
819
2995
                self.buffer.push_str(lt.newline.as_str());
820
            }
821
        }
822
    }
823
}
824

            
825
14985
fn escape_xml(s: &str) -> String {
826
14985
    s.replace('&', "&amp;")
827
14985
        .replace('<', "&lt;")
828
14985
        .replace('>', "&gt;")
829
}
830

            
831
3095
fn encode_html(s: &str) -> String {
832
3095
    s.replace('>', "&gt;").replace('<', "&lt;")
833
}
834

            
835
285
fn format_lines(s: &str, use_multiline_class: bool) -> String {
836
285
    regex::Regex::new(r"\n|\r\n")
837
285
        .unwrap()
838
285
        .split(s)
839
5462
        .map(|l| {
840
5405
            let text = escape_xml(l);
841
5405
            if use_multiline_class {
842
1760
                format!("<span class=\"line\"><span class=\"multiline\">{text}</span></span>")
843
            } else {
844
3645
                format!("<span class=\"line\">{text}</span>")
845
            }
846
5462
        })
847
285
        .collect::<Vec<String>>()
848
285
        .join("\n")
849
}
850

            
851
#[cfg(test)]
852
mod tests {
853
    use super::*;
854
    use crate::ast::{JsonObjectElement, MultilineStringKind, SourceInfo, TemplateElement};
855
    use crate::reader::Pos;
856
    use crate::typing::ToSource;
857

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

            
909
        let mut fmt = HtmlFormatter::new();
910
        fmt.fmt_multiline_string(&multiline_string, false);
911
        assert_eq!(
912
            fmt.buffer,
913
            "<span class=\"multiline\">```</span>\
914
        </span>\n\
915
        <span class=\"line\">\
916
            <span class=\"multiline\">line1</span>\
917
        </span>\n\
918
        <span class=\"line\">\
919
            <span class=\"multiline\">line2</span>\
920
        </span>\n\
921
        <span class=\"line\">\
922
            <span class=\"multiline\">```</span>"
923
        );
924
    }
925

            
926
    #[test]
927
    fn test_multilines() {
928
        assert_eq!(
929
            format_lines("{\n   \"id\": 1\n}", false),
930
            "<span class=\"line\">{</span>\n\
931
            <span class=\"line\">   \"id\": 1</span>\n\
932
            <span class=\"line\">}</span>"
933
        );
934
        assert_eq!(
935
            format_lines("{\n   \"id\": 1\n}", true),
936
            "<span class=\"line\"><span class=\"multiline\">{</span></span>\n\
937
            <span class=\"line\"><span class=\"multiline\">   \"id\": 1</span></span>\n\
938
            <span class=\"line\"><span class=\"multiline\">}</span></span>"
939
        );
940

            
941
        assert_eq!(
942
            format_lines(
943
                "<?xml version=\"1.0\"?>\n\
944
            <drink>café</drink>",
945
                false
946
            ),
947
            "<span class=\"line\">&lt;?xml version=\"1.0\"?&gt;</span>\n\
948
            <span class=\"line\">&lt;drink&gt;café&lt;/drink&gt;</span>"
949
        );
950

            
951
        assert_eq!(
952
            format_lines("Hello\n", false),
953
            "<span class=\"line\">Hello</span>\n\
954
            <span class=\"line\"></span>"
955
        );
956
    }
957

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

            
990
    #[test]
991
    fn test_json_encoded_newline() {
992
        let mut fmt = HtmlFormatter::new();
993
        let value = JsonValue::String(Template::new(
994
            Some('"'),
995
            vec![TemplateElement::String {
996
                value: "\n".to_string(),
997
                source: "\\n".to_source(),
998
            }],
999
            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;"
        );
    }
}