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
1712
        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
20600
    fn fmt_span_open(&mut self, class: &str) {
97
20600
        self.buffer.push_str("<span class=\"");
98
20600
        self.buffer.push_str(class);
99
20600
        self.buffer.push_str("\">");
100
    }
101

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

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

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

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

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

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

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

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

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

            
183
1210
    fn fmt_section_value(&mut self, section_value: &SectionValue) {
184
1210
        match section_value {
185
3188
            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
291
            SectionValue::Captures(items) => items.iter().for_each(|item| self.fmt_capture(item)),
198
320
            SectionValue::Options(items) => {
199
884
                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
820
    fn fmt_entry_option(&mut self, option: &EntryOption) {
218
820
        self.fmt_lts(&option.line_terminators);
219
820
        self.fmt_span_open("line");
220
820
        self.fmt_space(&option.space0);
221
820
        self.fmt_string(option.kind.identifier());
222
820
        self.fmt_space(&option.space1);
223
820
        self.buffer.push(':');
224
820
        self.fmt_space(&option.space2);
225
820
        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
20
            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
35
            OptionKind::Verbose(value) => self.fmt_bool_option(value),
261
15
            OptionKind::VeryVerbose(value) => self.fmt_bool_option(value),
262
        };
263
820
        self.fmt_span_close();
264
820
        self.fmt_lt(&option.line_terminator0);
265
    }
266

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

            
274
55
    fn fmt_count(&mut self, count: Count) {
275
55
        match count {
276
45
            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
270
    fn fmt_capture(&mut self, capture: &Capture) {
351
270
        self.fmt_lts(&capture.line_terminators);
352
270
        self.fmt_span_open("line");
353
270
        self.fmt_space(&capture.space0);
354
270
        self.fmt_template(&capture.name);
355
270
        self.fmt_space(&capture.space1);
356
270
        self.buffer.push(':');
357
270
        self.fmt_space(&capture.space2);
358
270
        self.fmt_query(&capture.query);
359
270
        for (space, filter) in capture.filters.iter() {
360
50
            self.fmt_space(space);
361
50
            self.fmt_filter(filter);
362
        }
363
270
        self.fmt_space(&capture.space3);
364
270
        if capture.redact {
365
20
            self.fmt_string("redact");
366
        }
367
270
        self.fmt_span_close();
368
270
        self.fmt_lt(&capture.line_terminator0);
369
    }
370

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

            
375
3330
    fn fmt_query_value(&mut self, query_value: &QueryValue) {
376
3330
        let query_type = query_value.identifier();
377
3330
        self.fmt_span("query-type", query_type);
378
3330
        match query_value {
379
310
            QueryValue::Header { space0, name } => {
380
310
                self.fmt_space(space0);
381
310
                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
1800
            QueryValue::Jsonpath { space0, expr } => {
392
1800
                self.fmt_space(space0);
393
1800
                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
            | QueryValue::Ip
419
695
            | QueryValue::Redirects => {}
420
        }
421
    }
422

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
681
41310
    fn fmt_space(&mut self, space: &Whitespace) {
682
41310
        let Whitespace { value, .. } = space;
683
41310
        if !value.is_empty() {
684
15335
            self.buffer.push_str(value);
685
25975
        };
686
    }
687

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

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

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

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

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

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

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

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

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

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

            
810
10475
    fn fmt_lts(&mut self, line_terminators: &[LineTerminator]) {
811
13485
        for line_terminator in line_terminators {
812
3010
            self.fmt_span_open("line");
813
3010
            if line_terminator.newline.value.is_empty() {
814
5
                self.buffer.push_str("<br />");
815
            }
816
3010
            self.fmt_span_close();
817
3010
            self.fmt_lt(line_terminator);
818
        }
819
    }
820
}
821

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

            
828
3060
fn encode_html(s: &str) -> String {
829
3060
    s.replace('>', "&gt;").replace('<', "&lt;")
830
}
831

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

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

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

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

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

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

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

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

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