use std::path::Path;
use maud::Markup;
use maud::PreEscaped;
use maud::html;
use wdl_ast::AstNode;
use wdl_ast::AstToken;
use wdl_ast::v1::Decl;
use crate::links::PageLinkIndex;
use crate::meta::DESCRIPTION_KEY;
use crate::meta::DefinitionMeta;
use crate::meta::MaybeSummarized;
use crate::meta::MetaMap;
use crate::meta::MetaMapExt;
use crate::meta::MetaMapValueSource;
use crate::meta::summarize_if_needed;
const EXPR_MAX_LENGTH: usize = 80;
const EXPR_CLIP_LENGTH: usize = 50;
#[derive(Debug, Eq, PartialEq)]
pub(crate) struct Group(pub String);
impl Group {
pub fn display_name(&self) -> &str {
&self.0
}
pub fn id(&self) -> String {
self.0.replace(" ", "-").to_lowercase()
}
}
impl PartialOrd for Group {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Group {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
if self.0 == other.0 {
return std::cmp::Ordering::Equal;
}
if self.0 == "Common" {
return std::cmp::Ordering::Less;
}
if other.0 == "Common" {
return std::cmp::Ordering::Greater;
}
if self.0 == "Resources" {
return std::cmp::Ordering::Greater;
}
if other.0 == "Resources" {
return std::cmp::Ordering::Less;
}
self.0.cmp(&other.0)
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum InputOutput {
Input,
Output,
}
#[derive(Debug)]
pub(crate) struct Parameter {
decl: Decl,
meta: MetaMap,
io: InputOutput,
}
impl DefinitionMeta for Parameter {
fn meta(&self) -> &MetaMap {
&self.meta
}
}
impl Parameter {
pub fn new(decl: Decl, meta: MetaMap, io: InputOutput) -> Self {
Self { decl, meta, io }
}
pub fn name(&self) -> String {
self.decl.name().text().to_owned()
}
pub fn ty(&self) -> String {
self.decl.ty().to_string()
}
pub fn render_expr(&self, summarize: bool) -> Markup {
let expr = self
.decl
.expr()
.map(|expr| expr.text().to_string())
.unwrap_or("None".to_string());
if !summarize {
let mut lines = expr.lines();
let first_line = lines.next().expect("expr should have at least one line");
let common_indent = lines
.clone()
.map(|line| line.chars().take_while(|c| c.is_whitespace()).count())
.min()
.unwrap_or(0);
let remaining_expr = lines
.map(|line| line.chars().skip(common_indent).collect::<String>())
.collect::<Vec<_>>()
.join("\n");
let full_expr = if remaining_expr.is_empty() {
first_line
} else {
&format!("{first_line}\n{remaining_expr}")
};
return html! {
sprocket-code language="wdl" copyable expandable {
(full_expr)
}
};
}
match summarize_if_needed(expr, EXPR_MAX_LENGTH, EXPR_CLIP_LENGTH) {
MaybeSummarized::No(expr) => {
html! { code { (expr) } }
}
MaybeSummarized::Yes(summary) => {
html! {
div class="main__summary-container" {
code { (summary) }
"..."
button type="button" class="main__button main__expression-toggle" x-on:click="expr_expanded = !expr_expanded" {
b x-text="expr_expanded ? 'Hide full expression' : 'Show full expression'" {}
}
}
}
}
}
}
pub fn required(&self) -> Option<bool> {
match self.io {
InputOutput::Input => match self.decl.as_unbound_decl() {
Some(d) => Some(!d.ty().is_optional()),
_ => Some(false),
},
InputOutput::Output => None,
}
}
pub fn group(&self) -> Option<Group> {
self.meta()
.get("group")
.and_then(MetaMapValueSource::text)
.map(Group)
}
pub fn render_remaining_meta(&self, assets: &Path) -> Option<Markup> {
let authored = self.meta().render_authored_body(assets);
let remaining = self.meta().render_remaining(&[DESCRIPTION_KEY, "group"]);
if authored.is_none() && remaining.is_none() {
return None;
}
Some(html! {
@if let Some(authored) = authored {
(authored)
}
@if let Some(remaining) = remaining {
(remaining)
}
})
}
pub fn render(&self, assets: &Path, links: &PageLinkIndex, page_dir: &Path) -> Markup {
let show_expr = self.required() != Some(true);
html! {
div class="main__grid-row" x-data=(
if show_expr { "{ description_expanded: false, expr_expanded: false }" } else { "{ description_expanded: false }" }
) {
div class="main__grid-cell" {
code { (self.name()) }
}
div class="main__grid-cell" {
(links.render_type(&self.ty(), page_dir))
}
@if show_expr {
div class="main__grid-cell" { (self.render_expr(true)) }
}
div class="main__grid-cell" {
(self.meta().render_description(true))
}
div x-show="description_expanded" class="main__grid-full-width-cell" {
(self.meta().render_description(false))
}
@if show_expr {
div x-show="expr_expanded" class="main__grid-full-width-cell" {
(self.render_expr(false))
}
}
}
@if let Some(addl_meta) = self.render_remaining_meta(assets) {
div class="main__grid-full-width-cell" x-data="{ addl_meta_expanded: false }" {
div class="main__addl-meta-outer-container" {
button type="button" class="main__button" x-on:click="addl_meta_expanded = !addl_meta_expanded" {
b x-text="addl_meta_expanded ? 'Hide Additional Meta' : 'Show Additional Metadata'" {}
}
div x-show="addl_meta_expanded" class="main__addl-meta-inner-container" {
(addl_meta)
}
}
}
}
}
}
}
pub(crate) fn render_non_required_parameters_table<'a, I>(
params: I,
assets: &Path,
links: &PageLinkIndex,
page_dir: &Path,
) -> Markup
where
I: Iterator<Item = &'a Parameter>,
{
let params = params.collect::<Vec<_>>();
let third_col = if params.iter().any(|p| p.required().is_none()) {
"Expression"
} else {
"Default"
};
let rows = params
.iter()
.map(|param| param.render(assets, links, page_dir).into_string())
.collect::<Vec<_>>()
.join(&html! { div class="main__grid-row-separator" {} }.into_string());
html! {
div class="main__grid-container" {
div class="main__grid-non-req-param-container" {
div class="main__grid-header-cell" { "Name" }
div class="main__grid-header-cell" { "Type" }
div class="main__grid-header-cell" { (third_col) }
div class="main__grid-header-cell" { "Description" }
div class="main__grid-header-separator" {}
(PreEscaped(rows))
}
}
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use std::path::PathBuf;
use wdl_ast::Document;
use super::InputOutput;
use super::Parameter;
use crate::links::PageLinkIndex;
use crate::meta::MetaMap;
fn input_parameters(wdl: &str) -> Vec<Parameter> {
let (doc, _) = Document::parse(wdl, None);
let item = doc.ast().into_v1().unwrap().items().next().unwrap();
let workflow = item.into_workflow_definition().unwrap();
workflow
.input()
.unwrap()
.declarations()
.map(|decl| Parameter::new(decl.clone(), MetaMap::default(), InputOutput::Input))
.collect()
}
#[test]
fn renders_struct_type_as_link() {
let params = input_parameters(
r#"
version 1.2
workflow w {
input {
Scroll scroll
Int count
}
}
"#,
);
let links = PageLinkIndex::from_pages([("Scroll", PathBuf::from("Scroll-struct.html"))]);
let scroll = params.iter().find(|p| p.name() == "scroll").unwrap();
let html = scroll
.render(Path::new("assets"), &links, Path::new(""))
.into_string();
assert!(html.contains("href=\"Scroll-struct.html\""));
assert!(html.contains(">Scroll</a>"));
let count = params.iter().find(|p| p.name() == "count").unwrap();
let html = count
.render(Path::new("assets"), &links, Path::new(""))
.into_string();
assert!(!html.contains("href"));
assert!(html.contains("Int"));
}
#[test]
fn summarized_expression_toggle_has_leading_space() {
let params = input_parameters(
r#"
version 1.2
workflow w {
input {
String message = "this expression is intentionally long enough to require the full expression toggle in the table"
}
}
"#,
);
let html = params[0]
.render(
Path::new("assets"),
&PageLinkIndex::default(),
Path::new(""),
)
.into_string();
assert!(html.contains("main__expression-toggle"));
}
}