use crate::ast::visit::Visitor;
use crate::ast::{
Comment, Entry, FilterValue, JsonValue, Method, Placeholder, Regex, Request, Response,
Template, U64, Whitespace, visit,
};
use crate::ast::{
CookiePath, HurlFile, MultilineString, Number, PredicateFuncValue, QueryValue, StatusValue,
VersionValue,
};
use crate::types::{DurationUnit, SourceString, ToSource};
pub fn format(file: &HurlFile, standalone: bool) -> String {
let mut fmt = HtmlFormatter::new();
let body = fmt.format(file);
if standalone {
let css = include_str!("hurl.css");
format!(
r#"<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hurl File</title>
<style>
{css}
</style>
</head>
<body>
{body}
</body>
</html>
"#
)
} else {
body.to_string()
}
}
pub fn hurl_css() -> String {
include_str!("hurl.css").to_string()
}
struct HtmlFormatter {
buffer: String,
}
const HURL_BASE64_VALUE_CLASS: &str = "base64";
const HURL_BOOLEAN_CLASS: &str = "boolean";
const HURL_COMMENT_CLASS: &str = "comment";
const HURL_DURATION_UNIT: &str = "unit";
const HURL_ENTRY_CLASS: &str = "entry";
const HURL_HEX_CLASS: &str = "hex";
const HURL_FILENAME_CLASS: &str = "filename";
const HURL_FILTER_KIND_CLASS: &str = "filter-type";
const HURL_JSON_CLASS: &str = "json";
const HURL_LANG_CLASS: &str = "language-hurl";
const HURL_METHOD_CLASS: &str = "method";
const HURL_MULTILINESTRING_CLASS: &str = "multiline";
const HURL_NULL_CLASS: &str = "null";
const HURL_NUMBER_CLASS: &str = "number";
const HURL_NOT_CLASS: &str = "not";
const HURL_PLACEHOLDER_CLASS: &str = "expr";
const HURL_PREDICATE_TYPE_CLASS: &str = "predicate-type";
const HURL_QUERY_TYPE_CLASS: &str = "query-type";
const HURL_REGEX_CLASS: &str = "regex";
const HURL_REQUEST_CLASS: &str = "request";
const HURL_RESPONSE_CLASS: &str = "response";
const HURL_SECTION_HEADER_CLASS: &str = "section-header";
const HURL_STRING_CLASS: &str = "string";
const HURL_URL_CLASS: &str = "url";
const HURL_VERSION_CLASS: &str = "version";
const HURL_XML_CLASS: &str = "xml";
impl HtmlFormatter {
fn new() -> Self {
HtmlFormatter {
buffer: String::new(),
}
}
fn format(&mut self, file: &HurlFile) -> &str {
self.buffer.clear();
self.visit_hurl_file(file);
&self.buffer
}
fn pre_open(&mut self, class: &'static str) {
self.buffer.push_str("<pre><code class=\"");
self.buffer.push_str(class);
self.buffer.push_str("\">");
}
fn pre_close(&mut self) {
self.buffer.push_str("</code></pre>");
}
fn span_open(&mut self, class: &'static str) {
self.buffer.push_str("<span class=\"");
self.buffer.push_str(class);
self.buffer.push_str("\">");
}
fn span_close(&mut self) {
self.buffer.push_str("</span>");
}
fn push_source(&mut self, source: &SourceString) {
self.push_untrusted(source.as_str());
}
fn push_untrusted(&mut self, str: &str) {
let escaped = str
.replace('&', "&")
.replace('<', "<")
.replace('>', ">");
self.buffer.push_str(&escaped);
}
fn push_trusted(&mut self, str: &str) {
self.buffer.push_str(str);
}
}
impl Visitor for HtmlFormatter {
fn visit_base64_value(&mut self, _value: &[u8], source: &SourceString) {
self.span_open(HURL_BASE64_VALUE_CLASS);
self.push_source(source);
self.span_close();
}
fn visit_bool(&mut self, value: bool) {
self.span_open(HURL_BOOLEAN_CLASS);
self.push_trusted(&value.to_string());
self.span_close();
}
fn visit_cookie_path(&mut self, path: &CookiePath) {
self.span_open(HURL_STRING_CLASS);
self.push_source(&path.to_source());
self.span_close();
}
fn visit_comment(&mut self, comment: &Comment) {
self.span_open(HURL_COMMENT_CLASS);
self.push_source(&comment.to_source());
self.span_close();
}
fn visit_duration_unit(&mut self, unit: DurationUnit) {
self.span_open(HURL_DURATION_UNIT);
self.push_trusted(&unit.to_string());
self.span_close();
}
fn visit_entry(&mut self, entry: &Entry) {
self.span_open(HURL_ENTRY_CLASS);
visit::walk_entry(self, entry);
self.span_close();
}
fn visit_filename(&mut self, filename: &Template) {
self.span_open(HURL_FILENAME_CLASS);
self.push_source(&filename.to_source());
self.span_close();
}
fn visit_filter_kind(&mut self, kind: &FilterValue) {
self.span_open(HURL_FILTER_KIND_CLASS);
self.push_trusted(kind.identifier());
self.span_close();
}
fn visit_hex_value(&mut self, _value: &[u8], source: &SourceString) {
self.span_open(HURL_HEX_CLASS);
self.push_source(source);
self.span_close();
}
fn visit_hurl_file(&mut self, file: &HurlFile) {
self.pre_open(HURL_LANG_CLASS);
visit::walk_hurl_file(self, file);
self.pre_close();
}
fn visit_i64(&mut self, n: i64) {
self.span_open(HURL_NUMBER_CLASS);
self.push_trusted(&n.to_string());
self.span_close();
}
fn visit_json_body(&mut self, json: &JsonValue) {
self.span_open(HURL_JSON_CLASS);
self.push_source(&json.to_source());
self.span_close();
}
fn visit_literal(&mut self, lit: &'static str) {
self.push_trusted(lit);
}
fn visit_method(&mut self, method: &Method) {
self.span_open(HURL_METHOD_CLASS);
self.push_trusted(&method.to_string());
self.span_close();
}
fn visit_multiline_string(&mut self, string: &MultilineString) {
self.span_open(HURL_MULTILINESTRING_CLASS);
self.push_source(&string.to_source());
self.span_close();
}
fn visit_not(&mut self, identifier: &'static str) {
self.span_open(HURL_NOT_CLASS);
self.push_trusted(identifier);
self.span_close();
}
fn visit_null(&mut self, null: &'static str) {
self.span_open(HURL_NULL_CLASS);
self.push_trusted(null);
self.span_close();
}
fn visit_number(&mut self, number: &Number) {
self.span_open(HURL_NUMBER_CLASS);
self.push_source(&number.to_source());
self.span_close();
}
fn visit_placeholder(&mut self, placeholder: &Placeholder) {
self.span_open(HURL_PLACEHOLDER_CLASS);
self.push_source(&placeholder.to_source());
self.span_close();
}
fn visit_predicate_kind(&mut self, kind: &PredicateFuncValue) {
self.span_open(HURL_PREDICATE_TYPE_CLASS);
self.push_source(&kind.to_source());
self.span_close();
}
fn visit_query_kind(&mut self, kind: &QueryValue) {
self.span_open(HURL_QUERY_TYPE_CLASS);
self.push_trusted(kind.identifier());
self.span_close();
}
fn visit_request(&mut self, request: &Request) {
self.span_open(HURL_REQUEST_CLASS);
visit::walk_request(self, request);
self.span_close();
}
fn visit_response(&mut self, response: &Response) {
self.span_open(HURL_RESPONSE_CLASS);
visit::walk_response(self, response);
self.span_close();
}
fn visit_regex(&mut self, regex: &Regex) {
self.span_open(HURL_REGEX_CLASS);
self.push_source(®ex.to_source());
self.span_close();
}
fn visit_status(&mut self, value: &StatusValue) {
self.span_open(HURL_NUMBER_CLASS);
self.push_trusted(&value.to_string());
self.span_close();
}
fn visit_string(&mut self, value: &str) {
self.span_open(HURL_STRING_CLASS);
self.push_untrusted(value);
self.span_close();
}
fn visit_section_header(&mut self, name: &str) {
self.span_open(HURL_SECTION_HEADER_CLASS);
self.push_trusted(name);
self.span_close();
}
fn visit_template(&mut self, template: &Template) {
self.span_open(HURL_STRING_CLASS);
self.push_source(&template.to_source());
self.span_close();
}
fn visit_url(&mut self, url: &Template) {
self.span_open(HURL_URL_CLASS);
self.push_source(&url.to_source());
self.span_close();
}
fn visit_u64(&mut self, n: &U64) {
self.span_open(HURL_NUMBER_CLASS);
self.push_trusted(n.to_source().as_str());
self.span_close();
}
fn visit_usize(&mut self, n: usize) {
self.span_open(HURL_NUMBER_CLASS);
self.push_trusted(&n.to_string());
self.span_close();
}
fn visit_variable_name(&mut self, name: &str) {
self.push_trusted(name);
}
fn visit_version(&mut self, value: &VersionValue) {
self.span_open(HURL_VERSION_CLASS);
self.push_trusted(&value.to_string());
self.span_close();
}
fn visit_xml_body(&mut self, xml: &str) {
self.span_open(HURL_XML_CLASS);
self.push_untrusted(xml);
self.span_close();
}
fn visit_whitespace(&mut self, ws: &Whitespace) {
self.push_trusted(ws.as_str());
}
}
#[cfg(test)]
mod tests {
use crate::ast::visit::Visitor;
use crate::ast::{
JsonObjectElement, JsonValue, MultilineString, MultilineStringKind, SourceInfo, Template,
TemplateElement, Whitespace,
};
use crate::format::html::HtmlFormatter;
use crate::reader::Pos;
use crate::types::ToSource;
#[test]
fn test_multiline_string() {
let kind = MultilineStringKind::Text(Template {
delimiter: None,
elements: vec![TemplateElement::String {
value: "line1\nline2\n".to_string(),
source: "line1\nline2\n".to_source(),
}],
source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
});
let multiline_string = MultilineString {
space: Whitespace {
value: String::new(),
source_info: SourceInfo {
start: Pos { line: 1, column: 4 },
end: Pos { line: 1, column: 4 },
},
},
newline: Whitespace {
value: "\n".to_string(),
source_info: SourceInfo {
start: Pos { line: 1, column: 4 },
end: Pos { line: 2, column: 1 },
},
},
kind,
};
let mut fmt = HtmlFormatter::new();
fmt.visit_multiline_string(&multiline_string);
assert_eq!(
fmt.buffer,
"<span class=\"multiline\">```\nline1\nline2\n```</span>"
);
}
#[test]
fn test_json() {
let value = JsonValue::Object {
space0: String::new(),
elements: vec![JsonObjectElement {
space0: "\n ".to_string(),
name: Template::new(
Some('"'),
vec![TemplateElement::String {
value: "id".to_string(),
source: "id".to_source(),
}],
SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
),
space1: String::new(),
space2: " ".to_string(),
value: JsonValue::Number("1".to_string()),
space3: "\n".to_string(),
}],
};
let mut fmt = HtmlFormatter::new();
fmt.visit_json_body(&value);
assert_eq!(fmt.buffer, "<span class=\"json\">{\n \"id\": 1\n}</span>");
}
#[test]
fn test_json_encoded_newline() {
let value = JsonValue::String(Template::new(
Some('"'),
vec![TemplateElement::String {
value: "\n".to_string(),
source: "\\n".to_source(),
}],
SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
));
let mut fmt = HtmlFormatter::new();
fmt.visit_json_body(&value);
assert_eq!(fmt.buffer, "<span class=\"json\">\"\\n\"</span>");
}
#[test]
fn test_xml() {
let value = "<?xml version=\"1.0\"?>\n<drink>café</drink>";
let mut fmt = HtmlFormatter::new();
fmt.visit_xml_body(value);
assert_eq!(
fmt.buffer,
"<span class=\"xml\"><?xml version=\"1.0\"?>\n<drink>café</drink></span>"
);
}
#[test]
fn test_xml_escape() {
let mut fmt = HtmlFormatter::new();
fmt.push_untrusted("hello");
assert_eq!(fmt.buffer, "hello");
let mut fmt = HtmlFormatter::new();
fmt.push_untrusted("<?xml version=\"1.0\"?>");
assert_eq!(fmt.buffer, "<?xml version=\"1.0\"?>");
}
}