use std::fmt::Write;
use std::fs;
use base64::Engine;
use build_html::{Html, HtmlContainer, HtmlElement, HtmlPage, HtmlTag};
use uuid::Uuid;
use crate::prelude::{
Error, EvidenceData, EvidencePackage, MediaFile, Result as EVPResult, TestCase,
TestCasePassStatus,
};
use super::Exporter;
#[derive(Default)]
pub struct HtmlExporter;
impl Exporter for HtmlExporter {
fn export_name() -> String {
"HTML Document".to_string()
}
fn export_extension() -> String {
".html".to_string()
}
fn export_package(
&mut self,
package: &mut EvidencePackage,
path: std::path::PathBuf,
) -> EVPResult<()> {
let mut page = HtmlPage::new()
.with_title(html_escape::encode_text(package.metadata().title()))
.with_style(include_str!("html.css"))
.with_script_literal(include_str!("html.js"))
.with_stylesheet(
"https://unpkg.com/@highlightjs/cdn-assets@11.11.1/styles/default.min.css",
)
.with_script_link("https://unpkg.com/@highlightjs/cdn-assets@11.11.1/highlight.min.js")
.with_script_link(
"https://unpkg.com/@highlightjs/cdn-assets@11.11.1/languages/http.min.js",
);
let title = HtmlElement::new(HtmlTag::Heading1)
.with_raw(html_escape::encode_text(package.metadata().title()));
page.add_html(title);
let mut authors = String::new();
for author in package.metadata().authors() {
if let Some(email) = author.email() {
write!(authors, "{} <{}>, ", author.name(), email).unwrap();
} else {
write!(authors, "{}, ", author.name()).unwrap();
}
}
authors.pop();
authors.pop();
page.add_html(
HtmlElement::new(HtmlTag::ParagraphText)
.with_attribute("class", "authors")
.with_raw(html_escape::encode_text(&authors)),
);
if let Some(description) = package.metadata().description() {
page.add_html(
HtmlElement::new(HtmlTag::ParagraphText)
.with_raw(html_escape::encode_text(description)),
);
}
if let Ok(branding_img) = std::env::var("EA_BRAND_IMAGE") {
let data = fs::read(branding_img).map_err(|e| {
std::io::Error::other(format!("Failed to read company brand image: {e}"))
})?;
let src = format!(
"data:application/octet-stream;base64,{}",
base64::prelude::BASE64_STANDARD_NO_PAD.encode(data)
);
page.add_image(
src,
format!(
"{} Logo",
std::env::var("EA_BRAND_NAME").unwrap_or("Brand".to_string())
),
);
}
let test_cases: Vec<&TestCase> = package.test_case_iter()?.collect();
let mut first = true;
let mut test_case_elems = vec![];
let mut tab_container =
HtmlElement::new(HtmlTag::UnorderedList).with_attribute("class", "tabs");
for (idx, test_case) in test_cases.iter().enumerate() {
let mut tab_elem = HtmlElement::new(HtmlTag::ListElement)
.with_attribute("data-tab-index", idx)
.with_link(
format!("#tab{idx}"),
format!(
"{}{}",
match test_case.metadata().passed() {
None => "",
Some(TestCasePassStatus::Pass) => "✅ ",
Some(TestCasePassStatus::Fail) => "❌ ",
},
test_case.metadata().title()
),
);
if first {
tab_elem.add_attribute("class", "selected");
}
tab_container.add_html(tab_elem);
let elem = create_test_case_div(package.clone(), test_case)
.map_err(Error::OtherExportError)?
.with_attribute("data-tab-index", idx)
.with_attribute(
"class",
format!(
"tab-content {}",
if first {
first = false;
"selected"
} else {
""
}
),
);
test_case_elems.push(elem);
}
page.add_raw(r#"<label class="print-hide"><input type="checkbox" id="showAll" /> Show all cases in one page</label>"#);
page.add_html(tab_container);
for elem in test_case_elems {
page.add_html(elem);
}
fs::write(path, page.to_html_string())?;
Ok(())
}
fn export_case(
&mut self,
package: &mut EvidencePackage,
case: Uuid,
path: std::path::PathBuf,
) -> EVPResult<()> {
let mut page = HtmlPage::new()
.with_title(html_escape::encode_text(package.metadata().title()))
.with_style(include_str!("html.css"))
.with_script_literal(include_str!("html.js"));
let case = package
.test_case(case)?
.ok_or(Error::OtherExportError("Test case not found!".into()))?;
let elem = create_test_case_div(package.clone(), case).map_err(Error::OtherExportError)?;
page.add_html(elem);
fs::write(path, page.to_html_string())?;
Ok(())
}
}
fn create_test_case_div(
mut package: EvidencePackage,
test_case: &TestCase,
) -> Result<HtmlElement, Box<dyn std::error::Error>> {
tracing::debug!("Creating HTML element for test case {}", test_case.id());
let mut elem = HtmlElement::new(HtmlTag::Div);
let mut meta_elem = HtmlElement::new(HtmlTag::Div)
.with_attribute("class", "metadata")
.with_html(
HtmlElement::new(HtmlTag::Heading2)
.with_attribute("class", "title")
.with_raw(html_escape::encode_text(test_case.metadata().title())),
)
.with_html(
HtmlElement::new(HtmlTag::ParagraphText)
.with_attribute("class", "execution-time")
.with_raw(test_case.metadata().execution_datetime().to_rfc2822()),
);
match test_case.metadata().passed() {
None => (),
Some(s) => {
let s = match s {
TestCasePassStatus::Pass => "✅ Pass",
TestCasePassStatus::Fail => "❌ Fail",
};
meta_elem.add_html(
HtmlElement::new(HtmlTag::ParagraphText)
.with_attribute("class", "status")
.with_raw(s),
);
}
}
if let Some(fields) = test_case.metadata().custom() {
let mut dl = HtmlElement::new(HtmlTag::DescriptionList)
.with_attribute("class", "custom-metadata-fields");
for (key, value) in fields {
let field = package
.metadata()
.custom_metadata()
.as_ref()
.unwrap()
.get(key)
.unwrap();
dl.add_html(
HtmlElement::new(HtmlTag::DescriptionListTerm)
.with_attribute("class", "custom-metadata-field-name")
.with_raw(field.name()),
);
dl.add_html(
HtmlElement::new(HtmlTag::DescriptionListDescription)
.with_attribute("class", "custom-metadata-field-value")
.with_raw(value),
);
}
meta_elem.add_html(dl);
}
elem.add_html(meta_elem);
for evidence in test_case.evidence() {
if let Some(caption) = evidence.caption() {
elem.add_html(
HtmlElement::new(HtmlTag::ParagraphText)
.with_attribute("class", "caption")
.with_raw(html_escape::encode_text(caption)),
);
}
match evidence.kind().as_str() {
"text/plain" => {
let data = evidence.value().get_data(&mut package)?;
let text = String::from_utf8_lossy(data.as_slice());
for line in text.lines() {
elem.add_html(
HtmlElement::new(HtmlTag::ParagraphText)
.with_raw(html_escape::encode_text(line)),
);
}
}
"text/markdown" => {
let data = evidence.value().get_data(&mut package)?;
let text = String::from_utf8_lossy(data.as_slice());
elem.add_raw(markdown::to_html(&text));
}
"text/vnd.angel.http-data" => {
let data = evidence.value().get_data(&mut package)?;
let data = String::from_utf8_lossy(data.as_slice());
let data_parts = data
.split('\x1e')
.map(std::string::ToString::to_string)
.collect::<Vec<_>>();
let request = data_parts.first().cloned().unwrap_or_default();
let response = data_parts.get(1).cloned().unwrap_or_default();
elem.add_html(
HtmlElement::new(HtmlTag::Div)
.with_attribute("class", "http-container")
.with_html(
HtmlElement::new(HtmlTag::Div)
.with_attribute("class", "http-request")
.with_html(
HtmlElement::new(HtmlTag::PreformattedText).with_html(
HtmlElement::new(HtmlTag::CodeText)
.with_attribute("class", "language-http")
.with_raw(html_escape::encode_text(&request)),
),
),
)
.with_html(
HtmlElement::new(HtmlTag::Div)
.with_attribute("class", "http-response")
.with_html(
HtmlElement::new(HtmlTag::PreformattedText).with_html(
HtmlElement::new(HtmlTag::CodeText)
.with_attribute("class", "language-http")
.with_raw(html_escape::encode_text(&response)),
),
),
),
);
}
mime => {
if mime.starts_with("image/") {
let data = evidence.value().get_data(&mut package)?;
let media = MediaFile::from(data);
if let Some(mime) = media.mime_type() {
let data = base64::prelude::BASE64_STANDARD_NO_PAD.encode(media.data());
elem.add_html(
HtmlElement::new(HtmlTag::Image)
.with_attribute("src", format!("data:{mime};base64,{data}")),
);
}
} else {
let data = evidence.value().get_data(&mut package)?;
let data = base64::prelude::BASE64_STANDARD_NO_PAD.encode(data);
let mime = if let EvidenceData::Media { hash } = evidence.value() {
if let Some(media) = package.get_media(hash).ok().flatten() {
if let Some(mime) = media.mime_type() {
mime.to_string()
} else {
"application/octet-stream".to_string()
}
} else {
"application/octet-stream".to_string()
}
} else {
"application/octet-stream".to_string()
};
elem.add_html(
HtmlElement::new(HtmlTag::Div).with_html(
HtmlElement::new(HtmlTag::Link)
.with_attribute("href", format!("data:{mime};base64,{data}"))
.with_attribute(
"download",
evidence
.original_filename()
.clone()
.unwrap_or(String::new()),
)
.with_raw(&if let Some(filename) = evidence.original_filename() {
filename.clone()
} else {
"Unnamed file".to_string()
}),
),
);
}
}
}
}
Ok(elem)
}