use crate::output::comment;
use crate::output::diagnostic;
use crate::output::parse_result;
use crate::output::path;
use crate::output::tree;
use crate::output::type_system::data;
use crate::output::type_system::data::class::ParameterInfo;
use base64::{engine::general_purpose, Engine as _};
const HEADER1: &str = concat!(
r#"
<!DOCTYPE html>
<!-- Generated by Substrait validator; https://substrait.io/ -->
<html>
<head>
<style>
/*
Copy of license text for icon font
==================================
"#,
include_str!("fa-solid-900.woff2.LICENSE.txt"),
r#"
*/
@font-face {
font-family: 'Font Awesome 6 Free';
font-style: normal;
font-weight: 900;
font-display: block;
src: url("data:font/woff2;base64,"#
);
const FONT_AWESOME: &[u8] = include_bytes!("fa-solid-900.woff2");
const HEADER2: &str = concat!(
r#"") format("woff2");
}
"#,
include_str!("style.css"),
r#"
</style>
</head>
<body>
"#
);
const FOOTER: &str = r#"
<script>
function open_cards(element) {
if (element.tagName.toLowerCase() === 'details') {
element.open = true;
}
if (element.parentElement !== null) {
open_cards(element.parentElement);
}
}
function select() {
var hash = location.hash.substring(1);
if (hash) {
var details = document.getElementById(hash);
if (details) {
open_cards(details);
}
}
}
window.addEventListener('hashchange', select);
select();
</script>
</body>
</html>
"#;
#[derive(PartialOrd, Ord, PartialEq, Eq)]
enum Level {
Ok,
ChildWarning,
Warning,
ChildError,
Error,
}
impl From<diagnostic::Level> for Level {
fn from(level: diagnostic::Level) -> Self {
match level {
diagnostic::Level::Info => Level::Ok,
diagnostic::Level::Warning => Level::Warning,
diagnostic::Level::Error => Level::Error,
}
}
}
impl Level {
pub fn class(&self) -> &'static str {
match self {
Level::Ok => "ok",
Level::ChildWarning => "warn_child",
Level::Warning => "warn_here",
Level::ChildError => "error_child",
Level::Error => "error_here",
}
}
}
fn html_escape<S: AsRef<str>>(text: S) -> String {
let text = text.as_ref();
let mut result = String::with_capacity(text.len());
for c in text.chars() {
match c {
'&' => result += "&",
'<' => result += "<",
'>' => result += ">",
'"' => result += """,
'\'' => result += "'",
c => result.push(c),
}
}
result
}
fn url_encode<S: AsRef<str>>(text: S) -> String {
use std::fmt::Write;
let text = text.as_ref();
let mut result = String::with_capacity(text.len());
for c in text.chars() {
if c.is_alphanumeric() || "-._~!$&'()*+,;=:@".contains(c) {
result.push(c);
} else {
let mut buf = [0; 4];
for b in c.encode_utf8(&mut buf).as_bytes() {
write!(result, "%{:02x}", *b).unwrap();
}
}
}
result
}
fn path_encode<S: AsRef<str>>(text: S) -> String {
text.as_ref()
.chars()
.map(|c| match c {
'[' => '(',
']' => ')',
'<' => '(',
'>' => ')',
c => c,
})
.collect()
}
fn format_path(path: &path::PathBuf, index: Option<usize>) -> String {
if let Some(index) = index {
format!("{path}:{index}")
} else {
path.to_string()
}
}
fn format_reference_parameters(path: &path::PathBuf, index: Option<usize>) -> String {
let path = format_path(path, index);
format!(
"href=\"#{}\" title=\"{}\"",
html_escape(url_encode(path_encode(&path))),
html_escape(&path)
)
}
fn format_reference<S: std::fmt::Display>(
text: S,
path: &path::PathBuf,
index: Option<usize>,
) -> String {
format!("<a {}>{text}</a>", format_reference_parameters(path, index))
}
fn format_anchor(path: &path::PathBuf, index: Option<usize>) -> String {
format!(
"<a {} class=\"anchor\"></a>",
format_reference_parameters(path, index)
)
}
fn format_id(path: &path::PathBuf, index: Option<usize>) -> String {
format!(
"id=\"{}\"",
html_escape(url_encode(path_encode(format_path(path, index))))
)
}
fn format_span<S: std::fmt::Display>(class: &'static str, text: S) -> String {
format!(
"<span class=\"{class}\">{}</span>",
html_escape(text.to_string())
)
}
fn format_span_html<S: std::fmt::Display>(class: &'static str, html: S) -> String {
format!("<span class=\"{class}\">{}</span>", html)
}
fn format_diagnostic(
diag: &diagnostic::Diagnostic,
path: &path::PathBuf,
index: usize,
with_id: bool,
with_path: bool,
) -> String {
let cause = format_span(
"cause",
if with_path {
diag.to_string()
} else {
format!("{:#}", diag)
},
);
let cause = if &diag.path == path {
cause
} else {
format_reference(cause, &diag.path, None)
};
let id = if with_id {
let mut id = format_id(path, Some(index));
id.push(' ');
id
} else {
String::new()
};
let anchor = format_anchor(path, Some(index));
let class = match diag.adjusted_level {
diagnostic::Level::Info => "diag_info",
diagnostic::Level::Warning => "diag_warn",
diagnostic::Level::Error => "diag_error",
};
format!("<div {id}class=\"card {class}\">\n{cause}\n{anchor}\n</div>")
}
fn format_diagnostics(path: &path::Path, node: &tree::Node) -> (Vec<String>, diagnostic::Level) {
let mut html = vec![];
let mut level = diagnostic::Level::Info;
for (index, data) in node.data.iter().enumerate() {
match data {
tree::NodeData::Child(child) => {
let (sub_html, sub_level) =
format_diagnostics(&path.with(child.path_element.clone()), &child.node);
html.extend(sub_html);
level = std::cmp::max(level, sub_level);
}
tree::NodeData::Diagnostic(diag) => {
html.push(format_diagnostic(
diag,
&path.to_path_buf(),
index,
false,
true,
));
level = std::cmp::max(level, diag.adjusted_level);
}
_ => {}
}
}
(html, level)
}
fn format_comment_span(span: &comment::Span) -> String {
match &span.link {
None => html_escape(&span.text),
Some(comment::Link::Path(path)) => format_reference(html_escape(&span.text), path, None),
Some(comment::Link::Url(url)) => format!(
"<a href=\"{}\">{}</a>",
html_escape(url),
html_escape(&span.text)
),
}
}
fn format_comment(comment: &comment::Comment) -> String {
let mut result = String::new();
let mut p_open = false;
for element in comment.elements().iter() {
match element {
comment::Element::Span(span) => {
if !p_open {
result += "<p>";
p_open = true;
}
result += &format_comment_span(span);
}
comment::Element::NewLine => {
if p_open {
result += "</p>";
p_open = false;
}
}
comment::Element::ListOpen => {
if p_open {
result += "</p>";
p_open = false;
}
result += "<ul><li>";
}
comment::Element::ListNext => {
if p_open {
result += "</p>";
p_open = false;
}
result += "</li><li>";
}
comment::Element::ListClose => {
if p_open {
result += "</p>";
p_open = false;
}
result += "</li></ul>";
}
}
}
if p_open {
result += "</p>";
}
result
}
fn format_brief(brief: &comment::Brief) -> String {
let mut result = String::new();
for span in brief.spans().iter() {
result += &format_comment_span(span);
}
result
}
fn format_relation_tree(
path: &path::Path,
node: &tree::Node,
index: &mut usize,
is_root: bool,
in_expression: bool,
) -> Vec<String> {
let mut html = vec![];
let text = node
.brief
.as_ref()
.map(format_brief)
.unwrap_or_else(|| String::from("unknown"));
let is_relation = matches!(node.class, tree::Class::Relation);
let is_expression = matches!(node.class, tree::Class::Expression);
if is_relation {
if is_root {
html.push("<details class=\"relation_tree\">".to_string());
html.push(format!(
"<summary>Query/relation graph #{}</summary>",
*index
));
html.push("<ul class=\"tree\"><li><span class=\"root\">Sink</span><ul>".to_string());
};
html.push(format!(
"<li><span class=\"{}\">{text} ({})</span>",
if in_expression {
"subquery"
} else {
"data_source"
},
format_reference("link", &path.to_path_buf(), None)
));
}
let mut has_children = false;
for data in node.data.iter() {
if let tree::NodeData::Child(child) = data {
let sub_html = format_relation_tree(
&path.with(child.path_element.clone()),
&child.node,
index,
is_root && !is_relation,
(in_expression && !is_relation) || is_expression,
);
if !sub_html.is_empty() {
if is_relation && !has_children {
html.push("<ul>".to_string());
}
has_children = true;
html.extend(sub_html);
}
}
}
if is_relation {
if has_children {
html.push("</ul>".to_string());
}
html.push("</li>".to_string());
if is_root {
html.push("</ul></li></ul>".to_string());
html.push("</details>".to_string());
*index += 1;
}
}
html
}
fn format_data_type_card(content: &str) -> String {
format!(
"<div class=\"card data_type\">\n{}\n</div>",
html_escape(content),
)
}
fn format_data_type(prefix: &str, data_type: &data::Type) -> Vec<String> {
let mut html = vec![];
if data_type.parameters().is_empty() {
html.push(format_data_type_card(&format!("{prefix}: {:#}", data_type)));
} else {
html.push("<details class=\"data_type\">\n<summary>".to_string());
html.push(format!("{prefix}: {}", html_escape(data_type.to_string())));
html.push("</summary>".to_string());
for (index, parameter) in data_type.parameters().iter().enumerate() {
let name = data_type
.class()
.parameter_name(index)
.unwrap_or_else(|| "?".to_string());
html.push(format_data_type_card(&format!(
".{}: {}",
parameter.name.as_ref().unwrap_or(&name),
parameter
.value
.as_ref()
.map(ToString::to_string)
.unwrap_or_else(|| String::from("null"))
)));
}
html.push("</details>".to_string());
}
html
}
fn format_node_tree(
path: &path::Path,
unknown_subtree: bool,
node: &tree::Node,
) -> (Vec<String>, Level) {
let pathbuf = path.to_path_buf();
let id = format_id(&pathbuf, None);
let brief = if let Some(brief) = &node.brief {
format_span_html("brief", format_brief(brief))
} else {
String::from("")
};
let value = match &node.node_type {
tree::NodeType::ProtoMessage(proto_type) => {
format!("{brief} {}", format_span("type", proto_type))
}
tree::NodeType::ProtoPrimitive(proto_type, data) => {
format!(
"= {} {brief} {}",
format_span("value", data),
format_span("type", proto_type)
)
}
tree::NodeType::ProtoMissingOneOf => "?".to_string(),
tree::NodeType::NodeReference(num, target) => format_reference(
format!(
"= {} {brief} {}",
format_span("value", num),
format_span("type", "uint32, reference")
),
&target.path,
None,
),
tree::NodeType::ResolvedUri(uri) => {
format!(
"= {} {brief} {}",
format_span("value", uri),
format_span("type", "string, resolved as URI")
)
}
tree::NodeType::YamlMap => format!("{brief} {}", format_span("type", "YAML map")),
tree::NodeType::YamlArray => format!("{brief} {}", format_span("type", "YAML array")),
tree::NodeType::YamlPrimitive(data) => format!("= {}{brief}", format_span("value", data)),
tree::NodeType::AstNode => format!("{brief} {}", format_span("type", "AST node")),
};
let header = format!(
"{} {value} {}",
format_span("field", path.end_to_string()),
format_anchor(&pathbuf, None)
);
if node.data.is_empty() && node.summary.is_none() {
let class = if unknown_subtree { "unknown" } else { "ok" };
return (
vec![format!("<div {id} class=\"card {class}\">{header}</div>")],
Level::Ok,
);
}
let mut html = vec![String::new()];
let mut level = Level::Ok;
if let Some(ref summary) = node.summary {
html.push(format_comment(summary));
}
for (index, data) in node.data.iter().enumerate() {
match data {
tree::NodeData::Child(child) => {
let (sub_html, sub_level) = format_node_tree(
&path.with(child.path_element.clone()),
!child.recognized,
&child.node,
);
html.extend(sub_html);
level = std::cmp::max(level, sub_level);
}
tree::NodeData::Diagnostic(diag) => {
html.push(format_diagnostic(
diag,
&pathbuf,
index,
true,
diag.path != pathbuf,
));
level = std::cmp::max(level, diag.adjusted_level.into());
}
tree::NodeData::DataType(data_type) => {
html.extend(format_data_type(
if matches!(node.class, tree::Class::Relation) {
"Schema"
} else {
"Data type"
},
data_type,
));
}
tree::NodeData::Comment(comment) => {
html.push("<div class=\"card comment\">\n".to_string());
html.push(format_comment(comment));
html.push("\n</div>".to_string());
}
}
}
let class = if unknown_subtree {
"unknown"
} else {
level.class()
};
html[0] = format!("<details {id} class=\"{class}\">\n<summary>\n{header}\n</summary>");
html.push("</details>".to_string());
let level = match level {
Level::Error => Level::ChildError,
Level::Warning => Level::ChildWarning,
x => x,
};
(html, level)
}
pub fn export<T: std::io::Write>(
out: &mut T,
root_name: &'static str,
result: &parse_result::ParseResult,
) -> std::io::Result<()> {
let path = path::Path::Root(root_name);
let font_awesome_b64 = general_purpose::STANDARD.encode(FONT_AWESOME);
write!(out, "{HEADER1}{}{HEADER2}", font_awesome_b64)?;
writeln!(out, "<details class=\"relation_tree\" open=\"true\">")?;
writeln!(out, "<summary>Metadata</summary>")?;
writeln!(
out,
"<p>Checked using validator version {}</p>",
crate::version()
)?;
writeln!(
out,
"<p>Checked against Substrait version {}</p>",
crate::substrait_version()
)?;
writeln!(out, "</details>")?;
writeln!(out, "<details class=\"relation_tree\" open=\"true\">")?;
writeln!(out, "<summary>Relation graphs</summary>")?;
writeln!(
out,
"<div class=\"note\">Note: data flows upwards in these graphs.</div>"
)?;
let mut index = 0;
for s in format_relation_tree(&path, &result.root, &mut index, true, false) {
writeln!(out, "{s}")?;
}
writeln!(out, "</details>")?;
let (diag_html, level) = format_diagnostics(&path, &result.root);
let validity_class = match level {
diagnostic::Level::Info => "valid",
diagnostic::Level::Warning => "maybe_valid",
diagnostic::Level::Error => "invalid",
};
let validity_summary = match level {
diagnostic::Level::Info => "This plan is <span class=\"valid\">VALID</span>",
diagnostic::Level::Warning => "The validator was unable to determine validity",
diagnostic::Level::Error => "This plan is <span class=\"invalid\">INVALID</span>",
};
writeln!(
out,
"<details class=\"{}\" open=\"true\">",
Level::from(level).class()
)?;
writeln!(
out,
"<summary class=\"{validity_class}\">{validity_summary}</summary>"
)?;
if diag_html.is_empty() {
writeln!(
out,
"<div class=\"note\">No diagnostics were reported.</div>"
)?;
} else {
for s in diag_html {
writeln!(out, "{s}")?;
}
}
writeln!(out, "</details>")?;
for s in format_node_tree(&path, false, &result.root).0 {
writeln!(out, "{s}")?;
}
write!(out, "{FOOTER}")
}